Compare commits

..

112 Commits

Author SHA1 Message Date
Aaryan Khandelwal f5460ec420 fix: product updates modal 2024-11-27 14:25:34 +05:30
Aaryan Khandelwal 2e2a3dfef0 chore: core file changes 2024-11-27 14:25:34 +05:30
Aaryan Khandelwal eee13b26d1 chore: core file changes 2024-11-27 14:25:34 +05:30
sriram veeraghanta b04dd4dbcf fix: silo build errors 2024-11-27 13:17:02 +05:30
sriram veeraghanta d5e0f60c5f Merge pull request #1820 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-27 13:11:36 +05:30
sriram veeraghanta 35a2dffadb fix: merge conflicts 2024-11-27 13:08:48 +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
pushya22 80e44a5ec8 Merge pull request #1819 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-26 20:54:55 +05:30
pushya22 bff00274d8 Merge pull request #1817 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-26 20:52:58 +05:30
Prateek Shourya 9dbb2b26c3 fix: issue activity sort order componenet import (#6098) 2024-11-26 20:49:39 +05:30
Prateek Shourya 6ad25cb7f7 Merge branch 'preview' of github.com:makeplane/plane-ee into sync/ce-ee 2024-11-26 20:35:01 +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
sriram veeraghanta 91fdea5022 Merge pull request #1816 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-26 17:10:17 +05:30
sriram veeraghanta 5df9ee72d2 fix: merge conflicts 2024-11-26 17:09:58 +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
pushya22 98f49f8321 Merge pull request #1814 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-25 20:09:03 +05:30
Anmol Singh Bhatia 7469e67b71 fix: project view application error (#6091) 2024-11-25 20:05:03 +05:30
sriram veeraghanta 4073c8d6ae Merge branch 'canary' of github.com:makeplane/plane-ee into preview 2024-11-25 17:19:54 +05:30
Anmol Singh Bhatia 45db2d5d3c chore: de dupe improvements (#1719) 2024-11-25 17:19:35 +05:30
sriram veeraghanta 974c47d534 Merge branch 'canary' of github.com:makeplane/plane-ee into preview 2024-11-25 16:32:24 +05:30
sriram veeraghanta 05bb8c6c3d fix: caddy file updated with graphql endpoint 2024-11-25 16:30:58 +05:30
sriram veeraghanta 1cb16bf176 fix: email error handling on magic auth 2024-11-25 15:02:50 +05:30
Henit Chobisa dbe330e304 feat: silo importers and integrations (#1811)
* SIlo's GitHub and Gitlab Integrations, with Sync and Magic Words  (#1585)

* fix: silo service initial setup

* feat: moved controllers inside runner

* feat: created core definations for apps and context

* feat: added interfaces required for apps

* feat: moved shared definations

* feat: added logger as an injection for apps

* feat: changes to controllers

* feat: added vm based app execution inside manager

* feat: added dummy implementations for core constructs

* feat: moved controllers to routes

* feat: created worker moduler for processing message queue requests

* feat: added task manager defination

* feat: added context and manager separations

* feat: created defaults for the defination implementation

* feat: added async task management inside apps

* feat: removed unnecessary arguments in function calls

* feat: added context builder for app instances

* feat: added runtime type safety with zod

* feat: added packaged definition structure for silo apps

* feat: added package based configuration for engine and definitions

* feat: added custom eslint rule for throws decorator

* feat: added custom eslint rule for try catch

* feat: removed custom eslint config

* feat: added authentication controller inside engine

* feat: added auth in silo engine

* feat: migrated jira ui module to plane

* chore: removed jira app

* chore: moved engine components into src_engine directory

* feat: added base template inside new src directory

* feat: migrated worker interface with mq and redis store inside new src

* feat: created jira migrator and jira importer types

* chore: migrated worker's helper inside the base directory

* feat: added logic for booting up root worker

* feat: added jira and jira auth service inside jira app

* feat: added all transformers inside jira package

* feat: added authorization types for jira

* feat: added jira authentication oauth class

* feat: added jira transformer and pull mechanisms inside jira app

* feat: addded batching logic inside jira migrator

* feat: embedded silo sdk inside jira migrator

* feat: added plane migrator inside engine

* feat: added plane migrator for push inside migration controller

* feat: added controller methods for migration

* feat: added credentials and job routes in controller

* feat: added linear importer

* feat: added linear pull function as importer

* feat: added transformation for linear data

* feat: added pull mechanism for linear

* feat: attached linear data importer with migration controller

* fix: removed hardcoded jira from cycle and module migrator

* feat: fixed build errors

* chore: addeed example env

* feat: added authentication routes for jira

* feat: added linear route controller

* fix: restructuring

* fix: sdk configs setup

* fix: merge conflicts

* fix: sdk setup

* chore: added jira and linear importers and separate packages

* feat: moved transformation parts to linear package

* feat: decoupled jira logical parts with worker

* fix: linear silo app to use linear package

* fix: build errors and dependency resolution with packages in silo

* fix: module build errors in silo

* fix: linear authorization flow

* feat: added logic for segregated workers

* feat: attached task manager with the base starter

* feat: added migrations, query and schema into db directory

* feat: added linear importer and jira importer app structure

* feat: added silo core package

* chore: migrated worker and main engine controller inside apps

* fix: made linear integration working

* silo: added cors

* feat: added base64 state changes with jira

* chore: updated silo env

* fix: jira token cookie

* feat: added issue attachments in linear job

* feat: added credentials controller

* feat: added github package inside silo packages

* feat: added resource fetching in jira api service

* feat: added credentials locking

* feat: created resources endpoint for jira

* feat: added endpoints for getting jira data

* fix: credentials not working

* chore: exported jira types

* chore: added jira states

* fix: jira project pagination issue

* chore: initiated silo folder in web

* feat: added github routes and services in silo app

* feat: improvided auth service and added background worker for github

* feat: added task manager for github

* fix: build updates

* fix: updated plane sdk and updated jira importer

* chore: updated the importer layout

* chore: removed as any from table component

* chore: integrated importer dashboard

* fix: tsup fixes

* fix: removed unnecessary files

* fix: removing tsup for building silo packages

* fix: build related issues

* feat: modified project and issue sdk service to add more methods

* feat: added installation webhook hook handler

* feat: added issue comment handler

* feat: added issue events handler interface

* feat: added pull request interface handler

* feat: added github worker for handling bg tasks

* feat: attached manager into main controller

* fix: build issues

* fix: eslint fixes

* fix: silo build errors

* fix: silo app build errors types

* fix: reverting the cloud branch

* fix: updated package json in silo service

* fix: branch build cloud updated

* fix: build errors in apps while using sdk due to ts-alias paths

* fix: branch build cloud workflow fixes

* feat: added user mentions with github sync

* fix: docker compose setup updates

* fix: bot user comments with github

* feat: initialized github plane worker

* fix: docker compose build fixes

* fix: docker build fixes envs updated in example file

* feat: added plane github webhook worker

* chore: updates folder structure and handled job services

* chore: resolved build errors in silo chore

* fix: docker compose cloud added

* feat: added issue handler with labels

* fix: build process docker compose

* feat: added issue edit and comment edits in github sync

* chore: updated jira workflow

* feat: added plane webhook types

* feat: added plane webhook event handlers

* feat: added workspace connection schema

* chore: handled the job start and jira dashboard

* feat: added query for getting entity connection

* chore: updated constants, file naming convention

* chore: resolved merge conflicts

* feat: added worspace and entity connection tables

* feat: added workspace and entity connections for pr, comment as well

* fix: github import with usermapping

* feat: added plane issue synce to github

* chore: integrated linear and updated jobs query

* feat: added hostname changes

* conflict: updated jira config and added issue transformation count in dashboard

* conflict: updated job

* chore: updated Jira status

* chore: updated Jira status

* fix: batch processing

* fix: batch key release

* chore: updated workflow for building images

* fix: detached silo build

* chore: updates linear and resolved build errors

* fix: batch key missing

* fix: merge issues with Credential types

* fix: issues with github mereg

* fix: linear workflow

* chore: updated linear queries

* feat: updated plane bg tasks to send webhook on desc change

* feat: added plane transformations for github

* feat: added connection schema and comment handler inside github worker

* chore: modified dashboard labels

* fix: docker compose fixed for running silo services

* feat: added key based deduping for webhook events

* feat: added schema based tables

* feat: added redis based payload dispatch and dedup

* feat: added migrations and changed connection table schema

* feat: modified github etl for changed webhook process

* feat: implemented redis ttl based arch with github

* feat: implemented gitlab silo package

* feat: added github user authentication

* feat: modified linear dashboard and services

* feat: changed yarn lock

* feat: added user auth into comments as well

* feat: addded gitlab authentication service

* fix: state activity not being reconized by external apis

* feat: added gitlab package for apis and sdk

* feat: added gitlab merge request integration

* fix: code cleanup for gitlab

* feat: added title references to gitlab

* feat: added magic words in github integration

* chore: removed logging token response

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>

* fix: env variables

* fix: silo schema migrations

* fix: drizzle migrations

* fix: adding migrator script

* fix: migrator command update in migrator

* fix: migrator changes

* fix: wait db updated

* chore: silo imports integrations (#1610)

* fix: silo service initial setup

* feat: moved controllers inside runner

* feat: created core definations for apps and context

* feat: added interfaces required for apps

* feat: moved shared definations

* feat: added logger as an injection for apps

* feat: changes to controllers

* feat: added vm based app execution inside manager

* feat: added dummy implementations for core constructs

* feat: moved controllers to routes

* feat: created worker moduler for processing message queue requests

* feat: added task manager defination

* feat: added context and manager separations

* feat: created defaults for the defination implementation

* feat: added async task management inside apps

* feat: removed unnecessary arguments in function calls

* feat: added context builder for app instances

* feat: added runtime type safety with zod

* feat: added packaged definition structure for silo apps

* feat: added package based configuration for engine and definitions

* feat: added custom eslint rule for throws decorator

* feat: added custom eslint rule for try catch

* feat: removed custom eslint config

* feat: added authentication controller inside engine

* feat: added auth in silo engine

* feat: migrated jira ui module to plane

* chore: removed jira app

* chore: moved engine components into src_engine directory

* feat: added base template inside new src directory

* feat: migrated worker interface with mq and redis store inside new src

* feat: created jira migrator and jira importer types

* chore: migrated worker's helper inside the base directory

* feat: added logic for booting up root worker

* feat: added jira and jira auth service inside jira app

* feat: added all transformers inside jira package

* feat: added authorization types for jira

* feat: added jira authentication oauth class

* feat: added jira transformer and pull mechanisms inside jira app

* feat: addded batching logic inside jira migrator

* feat: embedded silo sdk inside jira migrator

* feat: added plane migrator inside engine

* feat: added plane migrator for push inside migration controller

* feat: added controller methods for migration

* feat: added credentials and job routes in controller

* feat: added linear importer

* feat: added linear pull function as importer

* feat: added transformation for linear data

* feat: added pull mechanism for linear

* feat: attached linear data importer with migration controller

* fix: removed hardcoded jira from cycle and module migrator

* feat: fixed build errors

* chore: addeed example env

* feat: added authentication routes for jira

* feat: added linear route controller

* fix: restructuring

* fix: sdk configs setup

* fix: merge conflicts

* fix: sdk setup

* chore: added jira and linear importers and separate packages

* feat: moved transformation parts to linear package

* feat: decoupled jira logical parts with worker

* fix: linear silo app to use linear package

* fix: build errors and dependency resolution with packages in silo

* fix: module build errors in silo

* fix: linear authorization flow

* feat: added logic for segregated workers

* feat: attached task manager with the base starter

* feat: added migrations, query and schema into db directory

* feat: added linear importer and jira importer app structure

* feat: added silo core package

* chore: migrated worker and main engine controller inside apps

* fix: made linear integration working

* silo: added cors

* feat: added base64 state changes with jira

* chore: updated silo env

* fix: jira token cookie

* feat: added issue attachments in linear job

* feat: added credentials controller

* feat: added github package inside silo packages

* feat: added resource fetching in jira api service

* feat: added credentials locking

* feat: created resources endpoint for jira

* feat: added endpoints for getting jira data

* fix: credentials not working

* chore: exported jira types

* chore: added jira states

* fix: jira project pagination issue

* chore: initiated silo folder in web

* feat: added github routes and services in silo app

* feat: improvided auth service and added background worker for github

* feat: added task manager for github

* fix: build updates

* fix: updated plane sdk and updated jira importer

* chore: updated the importer layout

* chore: removed as any from table component

* chore: integrated importer dashboard

* fix: tsup fixes

* fix: removed unnecessary files

* fix: removing tsup for building silo packages

* fix: build related issues

* feat: modified project and issue sdk service to add more methods

* feat: added installation webhook hook handler

* feat: added issue comment handler

* feat: added issue events handler interface

* feat: added pull request interface handler

* feat: added github worker for handling bg tasks

* feat: attached manager into main controller

* fix: build issues

* fix: eslint fixes

* fix: silo build errors

* fix: silo app build errors types

* fix: reverting the cloud branch

* fix: updated package json in silo service

* fix: branch build cloud updated

* fix: build errors in apps while using sdk due to ts-alias paths

* fix: branch build cloud workflow fixes

* feat: added user mentions with github sync

* fix: docker compose setup updates

* fix: bot user comments with github

* feat: initialized github plane worker

* fix: docker compose build fixes

* fix: docker build fixes envs updated in example file

* feat: added plane github webhook worker

* chore: updates folder structure and handled job services

* chore: resolved build errors in silo chore

* fix: docker compose cloud added

* feat: added issue handler with labels

* fix: build process docker compose

* feat: added issue edit and comment edits in github sync

* chore: updated jira workflow

* feat: added plane webhook types

* feat: added plane webhook event handlers

* feat: added workspace connection schema

* chore: handled the job start and jira dashboard

* feat: added query for getting entity connection

* chore: updated constants, file naming convention

* chore: resolved merge conflicts

* feat: added worspace and entity connection tables

* feat: added workspace and entity connections for pr, comment as well

* fix: github import with usermapping

* feat: added plane issue synce to github

* chore: integrated linear and updated jobs query

* feat: added hostname changes

* conflict: updated jira config and added issue transformation count in dashboard

* conflict: updated job

* chore: updated Jira status

* chore: updated Jira status

* fix: batch processing

* fix: batch key release

* chore: updated workflow for building images

* fix: detached silo build

* chore: updates linear and resolved build errors

* fix: batch key missing

* fix: merge issues with Credential types

* fix: issues with github mereg

* fix: linear workflow

* chore: updated linear queries

* feat: updated plane bg tasks to send webhook on desc change

* feat: added plane transformations for github

* feat: added connection schema and comment handler inside github worker

* chore: modified dashboard labels

* fix: docker compose fixed for running silo services

* feat: added key based deduping for webhook events

* feat: added schema based tables

* feat: added redis based payload dispatch and dedup

* feat: added migrations and changed connection table schema

* feat: modified github etl for changed webhook process

* feat: implemented redis ttl based arch with github

* feat: implemented gitlab silo package

* feat: added github user authentication

* feat: modified linear dashboard and services

* feat: changed yarn lock

* feat: added user auth into comments as well

* feat: addded gitlab authentication service

* fix: state activity not being reconized by external apis

* feat: added gitlab package for apis and sdk

* feat: added gitlab merge request integration

* fix: code cleanup for gitlab

* feat: added title references to gitlab

* feat: added magic words in github integration

* chore: removed logging token response

* feat: added slack authentication flow

* feat: added user and team level authentication for slack

* feat: added services for slack integration

* feat: added blockkit components inside slack interaction

* feat: modified slack service for addded thread ephemeral messages

* feat: added handlers and views

* feat: asana importer.
* fix: build errors.

* feat: added thread sync to slack

* feat: added changes with merge

* chore: handle asana token refresh.

* fix: builder for giving warning

* chore: remove console logs.

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>

* fix: feature flags implementation

* fix: build errorsg

* fix: build errors

* chore: add missing silo packages.

* fix: updating package version for build issues in docker

* Added PAT based authentication for Jira Integration and Private bucket attachment uploads (#1642)

* feat: added pat authentication for jira service

* feat: added jira oauth and pat authentication strategy

* feat: added PAT auth for linear sdk

* feat: blocked routes for jira oauth

* fix: added OAuth Enabled key for authentication route

* feat: added PAT routes for linear and jira

* feat: added default value to job status

* feat: added credentials PAT filter

* feat: added credential fetch condition for PAT token

* chore: basic PAT token UI.

* fix: pat token implementation for jira

* chore: Jira personal access token form.

* fix: controller jira projects

* chore: handle migration steps without resourceId

* fix: jira project logo

* fix: migrator and attachment

* chore: update issue attachment external apis

* fix: attachments private bucket changes for silo

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>

* chore: Asana importer UI. (#1630)

* chore: UI improvements for Jira importer personal access token authentication (#1648)

* feat: added pat authentication for jira service

* feat: added jira oauth and pat authentication strategy

* feat: added PAT auth for linear sdk

* feat: blocked routes for jira oauth

* fix: added OAuth Enabled key for authentication route

* feat: added PAT routes for linear and jira

* feat: added default value to job status

* feat: added credentials PAT filter

* feat: added credential fetch condition for PAT token

* chore: basic PAT token UI.

* fix: pat token implementation for jira

* chore: Jira personal access token form.

* fix: controller jira projects

* chore: handle migration steps without resourceId

* fix: jira project logo

* fix: migrator and attachment

* chore: update issue attachment external apis

* fix: attachments private bucket changes for silo

* chore: minor UI improvements

* chore: ui improvement

* chore: fix importer list

---------

Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>

* fix: github package dependency with silo (#1622)

* fix/deps

* fix: axios

* chore: fix formatting.

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>

* Runway silo store (#1672)

* chore: silo store init

* chore: infra update for maintenance mode (#5963)

* [WEB-2728] improvement: add `true-transparent` variant for textarea. (#5960)

* fix: renaming and formatting

* chore: app sidebar behaviour (#5964)

* chore: infra update for maintenance mode (#1667)

* [WEB-2728] improvement: new UI for custom property settings. (#1663)

* chore: importer store integration for jira and resolved build error

---------

Co-authored-by: pushya22 <130810100+pushya22@users.noreply.github.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>

* Basic Jira issue type and custom properties migration (#1676)

* chore: issue type migration.

* chore: update issue types api for pagination

* chore: update jira migrator for issue types

* chore: issue type sdk types and service updates

* typo: issue service files name update

* typo: issue service files name update

* fix: issue type fetch and create request.

* fix: build

* fix: issue type migration

* chore: minor improvements

* chore: issue property fetch for jira

* chore: issue property fetch

* fix: type errors

* chore: temp fix for batches completion logic.

* chore: add issue type check

* chore: issue property basic migration.

* fix: updated base silo base url

* remove console log.

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>

* chore: add silo base url and silo base path in dockerfile

* chore: update in runway silo

* chore: custom field attributes structure.

* chore: helper for test settings config.

* chore: add logo props

* chore: custom field migrations

* chore: all simple properties migrations.

* chore: issue property options

* chore: update could not fetch feature

* chore: feature flag service in packages silo core

* chore: feature flagging

* chore: issue property values.

* [WEB-2742] chore: issue link ui revamp (#5971)

* chore-issue-link-ui

* chore: issue link ui revamp

* chore: upload image in the intake form (#1677)

* fix: editor user config (#1679)

* fix: editor user config (#5974)

* fix: pi chat bug fixes (#1670)

* chore: pi empty state

* fix: response change handled

* fix: handled api failure

* fix: formatted ai messages

* fix: reverted the pi pallete

* fix: integrated  models api

* fix: disabled regenerating new chat

* fix: page loading fixed

* fix: dynamically imported markdown

* fix: removed extras

* fix: minor css

* fix: handled pi chat in bulk ops

* [WEB-2706] fix: Add fallback when db initialisation fails (#5973)

* Add fallback when db initialization fails

* add checks for instance.exec

* chore: convert issue boolean fields to actual boolean value.

* change instance exec code

* sync issue to local db when inbox issue is accepted and draft issue is moved to project

* chore: added project and workspace keys

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>

* fix related to activity (#5972)

* fix: renamed inbox to intake (#5967)

* feat: intake

* chore: intake model migration changes

* dev: update dummy data

* dev: add duplicate apis for inbox

* dev: fix external apis

* fix: external apis

* chore: migration file changes

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>

* chore: removed the validation in patch request (#1680)

* chore: changed inbox to intake (#1682)

* chore: fix custom fields option fetch logic.

* fix: renamed inbox to intake (#5967)

* feat: intake

* chore: intake model migration changes

* dev: update dummy data

* dev: add duplicate apis for inbox

* dev: fix external apis

* fix: external apis

* chore: migration file changes

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>

* fix: intake loading (#5966)

* fix: intake loading

* fix: image upload in space

* chore: silo feature flag service

* fix: jira issue pull logic

* chore: map user picker custom property using display names.

* fix: silo github package dependencies.

* fix: silo github package

* chore: add lucide icons mappings

* fix: removed constants

* fix: package.json dependency fix for silo

* chore: update batching logic to handle trees

* chore: take job hostname from .env

* fix: build issues

* chore: fix migrations

* chore: asana custom field import

* chore: remove unused values

* chore: update oauth enabled envs

* chore: fix builds

* chore: asana batching logic.

* chore: asana PAT auth endpoint

* chore: delete credential endpoint

* fix: silo jira importer

* chore: fix silo refresh token callback

* chore: update linear batching logic

* dev: jira store migration

* chore: fetching jobs

* chore: updating job store

* chore: build error in store

* chore: job store updates

* chore: returning the jira resource details

* chore: add pat token validation

* chore: updated URL in the silo

* chore: issues count

* chore: fix merge conflicts

* dev: resolved merge conflict

* chore: removed logs

* feat: added comment import with linear

* chore: build fixes

* chore: add missing status class

* chore: updated Jira

* chore: loader in summary

* chore: Asana importer improved UI with mobx store

* fix: jobs api

* chore: make dashboard view true

* chore: updating the jira dashboard toggler

* chore: minor ui improvements.

* chore: fix job order logic.

* chore: Linear integration

* fix: slack linkback workflow

* chore: update service token logic

* chore: deactivate credentials

* dev: updated linear states mapping

* chore: asana priority mapping.

* chore: updated linear config

* fix:images not loading when issue is imported

* chore: asana comments

* chore: asana attachements

* chore: linear attachements pull

* chore: deactivating the importers

* chore: minor bug fix in priority mapping

* chore: handled feature flag, reload jobs and dashboard load

* chore: handled verfication check in start and re-run job

* chore: code cleanup

* feat: added controller routes for disconnecting and getting current status

* chore: update silo to be a single service

* fix: credentials

* chore: handled map states and map priority on worksapce and project change of importers

* chore: handled loaders in importers

* chore: updating linear team details

* chore: update assets

* chore: replaced migration

* fix: condition for credential availability

* chore: slack integration

* chore: handled modal confirmation and linear issue count in summary

* chore: github UI (#1721)

* chore: typos integrations and importers

* chore: github integration base

* fix: github integration context updates

* chore: resolved build errors

* fix: issue migrator to handle linear image component

* fix: issue serializer fixes

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: pushya22 <130810100+pushya22@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
2024-11-23 23:38:31 +05:30
sriram veeraghanta a8eb472a53 Merge pull request #1813 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-22 20:25:59 +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
sriram veeraghanta 58e8fda3ce Merge pull request #1812 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-22 17:01:09 +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 9fb5720dfd Merge pull request #1808 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-22 15:57:40 +05:30
guru_sainath 672e758e88 chore: handled issue filters duplicates (#1809) 2024-11-22 13:37:41 +05:30
pablohashescobar c4c5ed4282 Merge branch 'preview' of github.com:makeplane/plane-ee into sync/ce-ee 2024-11-22 12:29:01 +05:30
sriram veeraghanta b72d18079f fix: adding start and target date in issue exporter 2024-11-21 19:24:39 +05:30
guru_sainath 6e2fe6b852 [MOBIL-530] chore: added role filed in workspace type (#1806)
* chore: added role filed in workspace type

* chore: added user in the filter
2024-11-21 18:33:51 +05:30
guru_sainath c7ea909697 [MOBIL-508] fix: resolved sub-issue search for existing issues caused by parent_id (#1797)
* [MOBIL-515] chore: handled assignees and labels duplicates in issue detail (#1794)

* chore: handled the label and assignee duplication in issue types

* chore: updated order_by for labels and assignees

* [MOBIL-516] chore: updated issue type with parent_project_id and parent_project_identifier (#1793)

* chore: added parent issue project identifier in the issue detail query

* chore: parent project id and identifier in issue details

* chore: updated type of parent project id

* fix: subissue filtering by parent id
2024-11-21 18:33:23 +05:30
guru_sainath 5a3793ebc9 [MOBIL-516] chore: updated issue type with parent_project_id and parent_project_identifier (#1800)
* chore: added parent issue project identifier in the issue detail query

* chore: parent project id and identifier in issue details

* chore: updated type of parent project id
2024-11-21 18:32:44 +05:30
guru_sainath df70910807 [MOBIL-515] chore: handled assignees and labels duplicates in issue detail (#1801)
* chore: handled the label and assignee duplication in issue types

* chore: updated order_by for labels and assignees
2024-11-21 18:32:23 +05:30
Nikhil 43d871fbc8 Chore instance register script (#1802)
* chore: update instance register script to have updated values

* chore: formatting
2024-11-21 18:30:49 +05:30
Nikhil 35cdf5f027 fix: monitor healthcheck to handle closed connections (#1805) 2024-11-21 18:29:02 +05:30
sriram veeraghanta aa8002c333 fix: merge conflicts 2024-11-21 18:23:49 +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
pushya22 c4c58f67b9 Merge pull request #1804 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-21 15:17:53 +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
sriram veeraghanta b97a1bc71f fix: removing mobile branch sync from workflows 2024-11-20 13:38:42 +05:30
sriram veeraghanta a195444f82 Merge pull request #1799 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-20 13:28:32 +05:30
pablohashescobar ca91d5909b chore: formatting errors 2024-11-20 13:00:13 +05:30
pablohashescobar 3bea2e8d1b chore: fix instance apis 2024-11-20 12:35:13 +05:30
sriram veeraghanta a8887fe37f fix: setup instance edition changes 2024-11-20 00:55:19 +05:30
sriram veeraghanta 2ee10c81f8 fix: updating plane edition in register instance 2024-11-20 00:51:43 +05:30
sriram veeraghanta adebae9705 Merge pull request #1795 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-20 00:37:03 +05:30
sriram veeraghanta 429294fdc3 fix: merge conflicts 2024-11-20 00:36:13 +05:30
sriram veeraghanta 1325064676 fix: typo and naming conventions 2024-11-20 00:32:30 +05:30
sriram veeraghanta 40154e081e Merge pull request #1792 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-20 00:05:44 +05:30
sriram veeraghanta eed9635290 fix: merge conflicts 2024-11-20 00:05:26 +05:30
sriram veeraghanta a01a371767 fix: typo fixes 2024-11-20 00:00:04 +05:30
sriram veeraghanta 2d60337eac fix: celery timestamp changes 2024-11-19 20:17:53 +05:30
pablohashescobar f3ac26e5c9 chore: instances 2024-11-19 19:47:13 +05:30
sriram veeraghanta a8dc376585 Merge pull request #1790 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-19 18:32:14 +05:30
sriram veeraghanta a19422ba18 Merge branch 'preview' into sync/ce-ee 2024-11-19 18:31:57 +05:30
Aaryan Khandelwal d5a55de17a fix: cover image update fix for project and user profile (#6075)
* fix: cover image update payload

* fix: cover image assets

* chore: add gif support

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-11-19 18:28:53 +05:30
pushya22 6fe95c2452 Merge pull request #1789 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-19 17:10:50 +05:30
Prateek Shourya 6f497b024b [WEB-2770] fix: inbox issue detail loader on focus change (#6074) 2024-11-19 17:07:32 +05:30
sriram veeraghanta b2b3b7f43b Merge pull request #1788 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-19 15:56:39 +05:30
Nikhil dff4ca202f Merge branch 'preview' into sync/ce-ee 2024-11-19 15:52:21 +05:30
Nikhil a3e8ee6045 fix: remove caching for user based apis to handle avatar uploads (#6072) 2024-11-19 15:42:10 +05:30
Nikhil 1ce488aece remove: team fk from issue progress charts (#1785)
* remove: team fk from issue progress charts

* chore: migration
2024-11-18 14:18:26 +05:30
sriram veeraghanta 47c7bd2023 Merge pull request #1784 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-18 12:09:31 +05:30
sriram veeraghanta fc8133fd44 fix: deleting the dependabot file 2024-11-18 12:09:09 +05:30
sriram veeraghanta c1ac6e4244 chore: removing dependabot updates alerts 2024-11-18 12:06:13 +05:30
Bavisetti Narayan 037aec5750 chore: initiative migration (#1725) 2024-11-16 23:49:10 +05:30
sriram veeraghanta 7ea2d7ca76 Merge branch 'preview' of github.com:makeplane/plane-ee into mobile 2024-11-16 23:14:56 +05:30
guru_sainath 9782ee4046 chore: handled default issue type in mobile issue create mutation (#1761) 2024-11-16 20:44:14 +05:30
sriram veeraghanta fb0840bf75 Merge pull request #1760 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-16 19:50:34 +05:30
sriram veeraghanta ff507e282b fix: merge conflicts 2024-11-16 19:49:57 +05:30
sriram veeraghanta 58a40503f2 fix: merge conflicts 2024-11-16 19:49:07 +05:30
dependabot[bot] 6d98619082 chore(deps): bump actions/checkout from 3 to 4 (#6005)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:42:08 +05:30
dependabot[bot] 52d3169542 chore(deps): bump softprops/action-gh-release from 2.0.8 to 2.1.0 (#6010)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.8 to 2.1.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.0.8...v2.1.0)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:40:41 +05:30
dependabot[bot] 5989b1a134 chore(deps): bump github/codeql-action from 2 to 3 (#6011)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:39:31 +05:30
sriram veeraghanta 291bb5c899 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-11-16 19:37:22 +05:30
sriram veeraghanta 2ef00efaab fix: tubro repo upgrade 2024-11-16 19:37:06 +05:30
dependabot[bot] c5f96466e9 chore(deps): bump cross-spawn in the npm_and_yarn group (#6038)
Bumps the npm_and_yarn group with 1 update: [cross-spawn](https://github.com/moxystudio/node-cross-spawn).


Updates `cross-spawn` from 7.0.3 to 7.0.5
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 18:40:01 +05:30
sriram veeraghanta 35938b57af fix: dependabot security patch only 2024-11-16 18:36:47 +05:30
dependabot[bot] 1b1b160c04 chore(deps): bump docker/build-push-action from 5.1.0 to 6.9.0 (#6004)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.1.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.1.0...v6.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 18:30:44 +05:30
sriram veeraghanta 4149e84e62 Create dependabot.yml (#6002) 2024-11-16 18:25:29 +05:30
sriram veeraghanta 26c358deee fix: update dependabot yml 2024-11-16 18:17:16 +05:30
sriram veeraghanta 269e5fe5e5 Merge pull request #1716 from makeplane/sync/ce-ee
Sync: Community Changes
2024-11-16 18:00:57 +05:30
sriram veeraghanta 134d3dbf0b fix: dependabot fix 2024-11-16 17:58:56 +05:30
sriram veeraghanta 4bfbca031f fix: merge conflicts 2024-11-16 17:55:35 +05:30
Aaryan Khandelwal 9408e92e44 Revert "[WEB-1435] dev: conflict free issue descriptions (#5912)" (#6000)
This reverts commit e9680cab74.
2024-11-15 17:13:31 +05:30
Akshita Goyal 7f85c26297 chore: reverted planner (#1723) 2024-11-15 16:54:45 +05:30
Aaryan Khandelwal e9680cab74 [WEB-1435] dev: conflict free issue descriptions (#5912)
* chore: new description binary endpoints

* chore: conflict free issue description

* chore: fix submitting status

* chore: update yjs utils

* chore: handle component re-mounting

* chore: update buffer response type

* chore: add try catch for issue description update

* chore: update buffer response type

* chore: description binary in retrieve

* chore: update issue description hook

* chore: decode description binary

* chore: migrations fixes and cleanup

* chore: migration fixes

* fix: inbox issue description

* chore: move update operations to the issue store

* fix: merge conflicts

* chore: reverted the commit

* chore: removed the unwanted imports

* chore: remove unnecessary props

* chore: remove unused services

* chore: update live server error handling

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-11-15 16:38:58 +05:30
Akshita Goyal f0d463154d Chore project planner improvements (#1722)
* chore: planner status endpoint added and type updated

* feat: project planner status

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-11-15 15:42:47 +05:30
Akshita Goyal 939fdcbd97 chore: added fallback for pi api (#1718)
* chore: added fallback for pi api

* fix: removed trailing slash
2024-11-14 17:52:16 +05:30
guru_sainath 8c5ca529dd chore: made created_by and updated_by optional in types (#1715) 2024-11-14 16:09:20 +05:30
Akshita Goyal f6a5d80735 feat: project planner (#1710)
* feat: project planner

* fix: custom menu implementation

* fix: build

* fix: ce components

* fix: texts

* fix: changed text
2024-11-14 15:56:42 +05:30
sriram veeraghanta 229610513a fix: django instrumentation fixes 2024-11-13 21:04:16 +05:30
sriram veeraghanta f9d9c92c83 fix: opentelemetry sdk package update 2024-11-13 20:27:47 +05:30
guru_sainath 90602ad748 chore: workspace pages mutation and query (#1707)
* chore: workspace page mutation and query

* chore: updated query and mutation in schema

* chore: handled the auth and data in wrokspace pages
2024-11-13 19:08:18 +05:30
1035 changed files with 33793 additions and 21802 deletions
-11
View File
@@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
+2 -2
View File
@@ -83,7 +83,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: ./aio
file: ./aio/Dockerfile-base-full
@@ -124,7 +124,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: ./aio
file: ./aio/Dockerfile-base-slim
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
@@ -188,7 +188,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -422,7 +422,7 @@ jobs:
uses: actions/checkout@v4
- name: Generate Keypair
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
openssl genrsa -out private_key.pem 2048
else
echo "${{ secrets.DEFAULT_PRIME_PRIVATE_KEY }}" > private_key.pem
+2 -4
View File
@@ -25,9 +25,6 @@ on:
required: false
default: false
type: boolean
# push:
# branches:
# - master
env:
TARGET_BRANCH: ${{ github.ref_name }}
@@ -354,6 +351,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 }}
@@ -367,7 +365,7 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2.0.8
uses: softprops/action-gh-release@v2.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
+4 -4
View File
@@ -29,11 +29,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -46,7 +46,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -59,6 +59,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
+3 -10
View File
@@ -5,7 +5,6 @@ on:
push:
branches:
- "sync/**"
- "mobile-sync/**"
env:
CURRENT_BRANCH: ${{ github.ref_name }}
@@ -22,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all branches and tags
@@ -43,14 +42,8 @@ jobs:
- name: Create PR to Target Branch
run: |
# Determine target branch based on current branch prefix
if [[ "$CURRENT_BRANCH" == "${{vars.SYNC_TARGET_MOBILE_BRANCH_NAME}}" ]]; then
TARGET_BRANCH="mobile"
PR_TITLE="${{vars.SYNC_MOBILE_PR_TITLE}}"
else
TARGET_BRANCH="preview"
PR_TITLE="${{vars.SYNC_PR_TITLE}}"
fi
TARGET_BRANCH="preview"
PR_TITLE="${{vars.SYNC_PR_TITLE}}"
# get all pull requests and check if there is already a PR
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $CURRENT_BRANCH --state open --json number | jq '.[] | .number')
+2 -8
View File
@@ -5,7 +5,6 @@ on:
push:
branches:
- preview
- mobile
env:
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
@@ -18,7 +17,7 @@ jobs:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
@@ -38,12 +37,7 @@ jobs:
run: |
TARGET_REPO="${{ vars.SYNC_TARGET_REPO }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
if [ "$SOURCE_BRANCH" = "mobile" ]; then
TARGET_BRANCH="${{ vars.SYNC_TARGET_MOBILE_BRANCH_NAME }}"
else
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
fi
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
git remote add target-origin-a "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
+6 -1
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>
+1 -1
View File
@@ -195,7 +195,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
+1 -1
View File
@@ -191,7 +191,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
+1 -1
View File
@@ -192,7 +192,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
+7 -5
View File
@@ -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>
) : (
+4 -4
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>
+4 -3
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>
+2 -2
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>
+212
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 { 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";
// helpers
import { WEB_BASE_URL } from "@/helpers/common.helper";
// 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" });
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">{WEB_BASE_URL}/</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>
);
};
+21
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;
+12
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>;
}
+169
View File
@@ -0,0 +1,169 @@
"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";
// ui
import { Button, getButtonStyling, Loader, setPromiseToast, ToggleSwitch } from "@plane/ui";
// components
import { WorkspaceListItem } from "@/components/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
// 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;
@@ -9,8 +9,8 @@ import { getButtonStyling } from "@plane/ui";
import { cn } from "@/helpers/common.helper";
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>
);
@@ -52,13 +52,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">
@@ -4,7 +4,7 @@ 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";
import { Tooltip, WorkspaceIcon } from "@plane/ui";
// hooks
import { cn } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store";
@@ -14,31 +14,37 @@ 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/`,
},
];
+4
View File
@@ -33,6 +33,10 @@ export const InstanceHeader: FC = observer(() => {
return "Github";
case "gitlab":
return "GitLab";
case "workspace":
return "Workspace";
case "create":
return "Create";
default:
return pathName.toUpperCase();
}
+5 -7
View File
@@ -1,13 +1,13 @@
"use client";
import React from "react";
import { resolveGeneralTheme } from "helpers/common.helper";
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";
// 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>
+1
View File
@@ -0,0 +1 @@
export * from "./list-item";
@@ -0,0 +1,82 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { ExternalLink } from "lucide-react";
// helpers
import { Tooltip } from "@plane/ui";
import { WEB_BASE_URL } from "@/helpers/common.helper";
import { getFileURL } from "@/helpers/file.helper";
// 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 (
<Link
key={workspaceId}
href={encodeURI(WEB_BASE_URL + "/" + 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>
</Link>
);
});
+1
View File
@@ -1,3 +1,4 @@
export * from "./use-theme";
export * from "./use-instance";
export * from "./use-user";
export * from "./use-workspace";
+10
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;
};
+52
View File
@@ -0,0 +1,52 @@
// types
import type { IWorkspace, TWorkspacePaginationInfo } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// 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> {
return this.get(`/api/instances/workspace-slug-check/?slug=${slug}`)
.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;
});
}
}
+5
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);
}
}
+150
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: any) => 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 - any
*/
hydrate = (data: any) => {
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";
}
};
}
+7 -21
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)
+1 -3
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:
@@ -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,
)
+1 -1
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
+1 -5
View File
@@ -13,10 +13,6 @@ 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
from .issue_type import IssueTypeAPISerializer, ProjectIssueTypeAPISerializer
+1 -3
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
+2 -8
View File
@@ -23,9 +23,7 @@ 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")
return data
class Meta:
@@ -50,11 +48,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):
@@ -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)
+18 -61
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"]
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
@@ -264,13 +245,9 @@ class IssueSerializer(BaseSerializer):
]
if "labels" in self.fields:
if "labels" in self.expand:
data["labels"] = LabelSerializer(
instance.labels.all(), many=True
).data
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
else:
data["labels"] = [
str(label.id) for label in instance.labels.all()
]
data["labels"] = [str(label.id) for label in instance.labels.all()]
return data
@@ -278,11 +255,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 +307,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 +317,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 +358,7 @@ class IssueCommentSerializer(BaseSerializer):
"created_at",
"updated_at",
]
exclude = [
"comment_stripped",
"comment_json",
]
exclude = ["comment_json"]
def validate(self, data):
try:
@@ -407,38 +375,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):
+5 -13
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"}
+3 -11
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"]
+4 -9
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
+1 -5
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
+5
View File
@@ -71,4 +71,9 @@ urlpatterns = [
IssueAttachmentEndpoint.as_view(),
name="attachment",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="attachment",
),
]
+2 -4
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",
),
)
]
+2 -7
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>/",
+7 -18
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
+46 -138
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"
+25 -77
View File
@@ -38,16 +38,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(
@@ -56,8 +52,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:
@@ -65,8 +60,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,
@@ -79,41 +73,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:
@@ -133,8 +115,7 @@ 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
@@ -192,11 +173,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:
@@ -208,15 +185,10 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
)
intake_settings = IntakeSetting.objects.filter(
workspace__slug=slug,
project_id=project_id,
intake=intake,
workspace__slug=slug, project_id=project_id, intake=intake
).first()
if (
intake_settings is not None
and not intake_settings.is_in_app_enabled
):
if intake_settings is not None and not intake_settings.is_in_app_enabled:
return Response(
{"error": "Creating intake issues is disabled"},
status=status.HTTP_400_BAD_REQUEST,
@@ -258,7 +230,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())),
@@ -275,11 +247,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 = {
@@ -287,14 +255,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
@@ -334,14 +298,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
@@ -350,18 +310,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
@@ -370,9 +326,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),
@@ -384,13 +338,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):
@@ -398,10 +349,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:
+240 -244
View File
@@ -1,9 +1,10 @@
# Python imports
import json
from django.core.serializers.json import DjangoJSONEncoder
import uuid
# Django imports
from django.http import HttpResponseRedirect
from django.core.serializers.json import DjangoJSONEncoder
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,9 +51,13 @@ from plane.db.models import (
Project,
ProjectMember,
CycleIssue,
Workspace,
)
from plane.bgtasks.version_task import version_task
from plane.settings.storage import S3Storage
from .base import BaseAPIView
from plane.payment.flags.flag_decorator import check_workspace_feature_flag
from plane.payment.flags.flag import FeatureFlag
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
class WorkspaceIssueAPIEndpoint(BaseAPIView):
@@ -73,9 +78,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 +94,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 +107,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 +121,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 +155,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 +206,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 +254,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 +263,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 +307,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 +353,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 +411,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 +428,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,16 +437,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
)
# Serialize the existing instance
existing_instance = json.dumps(
{
"description_html": issue.description_html,
},
cls=DjangoJSONEncoder,
)
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
@@ -501,10 +446,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():
@@ -529,13 +471,6 @@ class IssueAPIEndpoint(BaseAPIView):
)
serializer.save()
# Return a success response
version_task.delay(
entity_type="ISSUE",
entity_identifier=pk,
existing_instance=existing_instance,
user_id=request.user.id,
)
issue_activity.delay(
type="issue.activity.updated",
requested_data=requested_data,
@@ -549,9 +484,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,
@@ -590,9 +523,7 @@ class LabelAPIEndpoint(BaseAPIView):
serializer_class = LabelSerializer
model = Label
permission_classes = [
ProjectMemberPermission,
]
permission_classes = [ProjectMemberPermission]
def get_queryset(self):
return (
@@ -639,12 +570,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,
@@ -665,18 +592,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):
@@ -719,9 +639,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
"""
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
model = IssueLink
serializer_class = IssueLinkSerializer
@@ -744,46 +662,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),
@@ -795,19 +699,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(
@@ -824,14 +722,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",
@@ -856,15 +750,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(
@@ -888,22 +778,44 @@ class IssueCommentAPIEndpoint(BaseAPIView):
)
def get(self, request, slug, project_id, issue_id, pk=None):
external_id = request.GET.get("external_id")
external_source = request.GET.get("external_source")
if external_id and external_source:
try:
issue_comment = IssueComment.objects.get(
external_id=external_id,
external_source=external_source,
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
)
serializer = IssueCommentSerializer(
issue_comment, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK)
except IssueComment.DoesNotExist:
return Response(
{"error": "Comment not found"}, status=status.HTTP_404_NOT_FOUND
)
if pk:
issue_comment = self.get_queryset().get(pk=pk)
serializer = IssueCommentSerializer(
issue_comment,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
try:
issue_comment = self.get_queryset().get(pk=pk)
serializer = IssueCommentSerializer(
issue_comment, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK)
except IssueComment.DoesNotExist:
return Response(
{"error": "Comment not found"}, status=status.HTTP_404_NOT_FOUND
)
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,
)
@@ -936,27 +848,20 @@ 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
)
issue_comment.save(update_fields=["created_at", "created_by"])
issue_comment.actor_id = request.data.get("created_by", request.user.id)
issue_comment.save(update_fields=["created_at", "created_by", "actor"])
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")),
@@ -968,24 +873,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,
@@ -1022,14 +920,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(
@@ -1045,9 +939,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 = (
@@ -1072,78 +964,125 @@ 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,
)
# Check if the file size is greater than the limit
if check_workspace_feature_flag(
feature_key=FeatureFlag.FILE_SIZE_LIMIT_PRO,
slug=slug,
user_id=str(request.user.id),
):
size_limit = min(size, settings.PRO_FILE_SIZE_LIMIT)
else:
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,
@@ -1152,9 +1091,66 @@ class IssueAttachmentEndpoint(BaseAPIView):
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)
+89 -34
View File
@@ -1,3 +1,6 @@
# Python imports
import random
# Django imports
from django.db.models import OuterRef, Subquery
from django.db.models.functions import Coalesce
@@ -26,6 +29,41 @@ class IssueTypeAPIEndpoint(BaseAPIView):
"""
logo_icons = [
"Activity",
"AlertCircle",
"Archive",
"Bell",
"Calendar",
"Camera",
"Check",
"Clock",
"Code",
"Database",
"Download",
"Edit",
"File",
"Folder",
"Globe",
"Heart",
"Home",
"Mail",
"Search",
"User",
]
logo_backgrounds = [
"#EF5974",
"#FF7474",
"#FC964D",
"#1FA191",
"#6DBCF5",
"#748AFF",
"#4C49F8",
"#5D407A",
"#999AA0",
]
model = IssueType
serializer_class = IssueTypeAPISerializer
permission_classes = [ProjectEntityPermission]
@@ -43,43 +81,59 @@ class IssueTypeAPIEndpoint(BaseAPIView):
def type_id(self):
return self.kwargs.get("type_id", None)
def generate_logo_prop(self):
return {
"in_use": "icon",
"icon": {
"name": self.logo_icons[
random.randint(0, len(self.logo_icons) - 1)
],
"background_color": self.logo_backgrounds[
random.randint(0, len(self.logo_backgrounds) - 1)
],
},
}
# list issue types and get issue type by id
@check_feature_flag(FeatureFlag.ISSUE_TYPE_DISPLAY)
def get(self, request, slug, project_id, type_id=None):
if self.workspace_slug and self.project_id:
# list of issue types
if self.type_id is None:
issue_types = self.model.objects.filter(
workspace__slug=self.workspace_slug,
project_issue_types__project_id=self.project_id,
).annotate(
project_ids=Coalesce(
Subquery(
ProjectIssueType.objects.filter(
issue_type=OuterRef("pk"), workspace__slug=slug
)
.values("issue_type")
.annotate(
project_ids=ArrayAgg(
"project_id", distinct=True
)
)
.values("project_ids")
),
[],
)
)
serializer = self.serializer_class(issue_types, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
# getting issue type by id
issue_type = self.model.objects.get(
# list of issue types
if self.type_id is None:
issue_types = self.model.objects.filter(
workspace__slug=self.workspace_slug,
project_issue_types__project_id=self.project_id,
pk=self.type_id,
).annotate(
project_ids=Coalesce(
Subquery(
ProjectIssueType.objects.filter(
issue_type=OuterRef("pk"), workspace__slug=slug
)
.values("issue_type")
.annotate(
project_ids=ArrayAgg("project_id", distinct=True)
)
.values("project_ids")
),
[],
)
)
serializer = self.serializer_class(issue_type)
return Response(serializer.data, status=status.HTTP_200_OK)
return self.paginate(
request=request,
queryset=(issue_types),
on_results=lambda issues: IssueTypeAPISerializer(
issues,
many=True,
).data,
)
# getting issue type by id
issue_type = self.model.objects.get(
workspace__slug=self.workspace_slug,
project_issue_types__project_id=self.project_id,
pk=self.type_id,
)
serializer = self.serializer_class(issue_type)
return Response(serializer.data, status=status.HTTP_200_OK)
# create issue type
@check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
@@ -116,10 +170,11 @@ class IssueTypeAPIEndpoint(BaseAPIView):
)
# creating issue type
data = request.data
issue_type_serializer = self.serializer_class(data=data)
issue_type_serializer = self.serializer_class(data=request.data)
issue_type_serializer.is_valid(raise_exception=True)
issue_type_serializer.save(workspace=workspace)
issue_type_serializer.save(
workspace=workspace, logo_props=self.generate_logo_prop()
)
# adding the issue type to the project
project_issue_type_serializer = ProjectIssueTypeAPISerializer(
+8 -29
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)
+31 -99
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)
+20 -67
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(
@@ -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):
+5 -17
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,
)
@@ -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,
)
+1 -1
View File
@@ -12,4 +12,4 @@ from .project import (
ProjectMemberPermission,
ProjectLitePermission,
)
from .base import allow_permission, ROLE
from .base import allow_permission, ROLE
+2 -3
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
+2 -6
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":
+4 -12
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()
+2 -10
View File
@@ -35,10 +35,7 @@ from .project import (
ProjectMemberRoleSerializer,
)
from .state import StateSerializer, StateLiteSerializer
from .view import (
IssueViewSerializer,
)
from .view import IssueViewSerializer
from .cycle import (
CycleSerializer,
CycleIssueSerializer,
@@ -72,8 +69,6 @@ from .issue import (
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
IssueDescriptionVersionSerializer,
IssueDescriptionVersionDetailSerializer,
)
from .module import (
@@ -115,10 +110,7 @@ from .intake import (
from .analytic import AnalyticViewSerializer
from .notification import (
NotificationSerializer,
UserNotificationPreferenceSerializer,
)
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
from .exporter import ExporterHistorySerializer
+1 -4
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", {})
+1 -6
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"]
+5 -12
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"] = []
+5 -24
View File
@@ -5,11 +5,7 @@ from rest_framework import serializers
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.ee.models import EntityProgress
@@ -20,20 +16,13 @@ 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")
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):
@@ -89,22 +78,14 @@ 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"]
class EntityProgressSerializer(BaseSerializer):
+8 -32
View File
@@ -23,22 +23,13 @@ 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
)
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
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
@@ -76,9 +67,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):
@@ -256,20 +245,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
@@ -301,11 +281,7 @@ class DraftIssueSerializer(BaseSerializer):
class DraftIssueDetailSerializer(DraftIssueSerializer):
description_html = serializers.CharField()
description_binary = serializers.CharField()
class Meta(DraftIssueSerializer.Meta):
fields = DraftIssueSerializer.Meta.fields + [
"description_html",
"description_binary",
]
fields = DraftIssueSerializer.Meta.fields + ["description_html"]
read_only_fields = fields
+5 -23
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"]
+1 -3
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
+2 -16
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"]
@@ -89,9 +77,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)
+2 -6
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
+6 -23
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)
+33 -214
View File
@@ -1,6 +1,3 @@
# Python imports
import base64
# Django imports
from django.utils import timezone
from django.core.validators import URLValidator
@@ -36,7 +33,6 @@ from plane.db.models import (
IssueVote,
IssueRelation,
State,
IssueDescriptionVersion,
IssueType,
)
@@ -66,12 +62,7 @@ class IssueProjectLiteSerializer(BaseSerializer):
class Meta:
model = Issue
fields = [
"id",
"project_detail",
"name",
"sequence_id",
]
fields = ["id", "project_detail", "name", "sequence_id"]
read_only_fields = fields
@@ -80,22 +71,13 @@ class IssueProjectLiteSerializer(BaseSerializer):
class IssueCreateSerializer(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
)
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
)
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()),
@@ -136,9 +118,7 @@ class IssueCreateSerializer(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):
@@ -160,9 +140,7 @@ class IssueCreateSerializer(BaseSerializer):
# Create Issue
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
@@ -267,9 +245,7 @@ class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueActivity
@@ -280,11 +256,7 @@ class IssueUserPropertySerializer(BaseSerializer):
class Meta:
model = IssueUserProperty
fields = "__all__"
read_only_fields = [
"user",
"workspace",
"project",
]
read_only_fields = ["user", "workspace", "project"]
class LabelSerializer(BaseSerializer):
@@ -299,30 +271,20 @@ class LabelSerializer(BaseSerializer):
"workspace_id",
"sort_order",
]
read_only_fields = [
"workspace",
"project",
]
read_only_fields = ["workspace", "project"]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = [
"id",
"name",
"color",
]
fields = ["id", "name", "color"]
class IssueLabelSerializer(BaseSerializer):
class Meta:
model = IssueLabel
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
read_only_fields = ["workspace", "project"]
class IssueRelationSerializer(BaseSerializer):
@@ -338,17 +300,8 @@ class IssueRelationSerializer(BaseSerializer):
class Meta:
model = IssueRelation
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
"project",
]
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
class RelatedIssueSerializer(BaseSerializer):
@@ -356,25 +309,14 @@ class RelatedIssueSerializer(BaseSerializer):
project_id = serializers.PrimaryKeyRelatedField(
source="issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(
source="issue.sequence_id", read_only=True
)
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta:
model = IssueRelation
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
"project",
]
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
class IssueAssigneeSerializer(BaseSerializer):
@@ -482,8 +424,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"}
@@ -493,8 +434,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()
@@ -522,7 +462,6 @@ class IssueLinkLiteSerializer(BaseSerializer):
class IssueAttachmentSerializer(BaseSerializer):
asset_url = serializers.CharField(read_only=True)
class Meta:
@@ -560,37 +499,20 @@ class IssueReactionSerializer(BaseSerializer):
class Meta:
model = IssueReaction
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"actor",
"deleted_at",
]
read_only_fields = ["workspace", "project", "issue", "actor", "deleted_at"]
class IssueReactionLiteSerializer(DynamicBaseSerializer):
class Meta:
model = IssueReaction
fields = [
"id",
"actor",
"issue",
"reaction",
]
fields = ["id", "actor", "issue", "reaction"]
class CommentReactionSerializer(BaseSerializer):
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"comment",
"actor",
"deleted_at",
]
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
class IssueVoteSerializer(BaseSerializer):
@@ -598,14 +520,7 @@ class IssueVoteSerializer(BaseSerializer):
class Meta:
model = IssueVote
fields = [
"issue",
"vote",
"workspace",
"project",
"actor",
"actor_detail",
]
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
read_only_fields = fields
@@ -613,9 +528,7 @@ class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
comment_reactions = CommentReactionSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)
@@ -639,25 +552,15 @@ class IssueStateFlatSerializer(BaseSerializer):
class Meta:
model = Issue
fields = [
"id",
"sequence_id",
"name",
"state_detail",
"project_detail",
]
fields = ["id", "sequence_id", "name", "state_detail", "project_detail"]
# Issue Serializer with state details
class IssueStateSerializer(DynamicBaseSerializer):
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
@@ -668,10 +571,7 @@ class IssueStateSerializer(DynamicBaseSerializer):
class IssueIntakeSerializer(DynamicBaseSerializer):
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
class Meta:
model = Issue
@@ -692,20 +592,11 @@ class IssueIntakeSerializer(DynamicBaseSerializer):
class IssueSerializer(DynamicBaseSerializer):
# 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)
# Count items
sub_issues_count = serializers.IntegerField(read_only=True)
@@ -748,41 +639,16 @@ class IssueSerializer(DynamicBaseSerializer):
class IssueLiteSerializer(DynamicBaseSerializer):
class Meta:
model = Issue
fields = [
"id",
"sequence_id",
"project_id",
"type_id",
]
fields = ["id", "sequence_id", "project_id", "type_id"]
read_only_fields = fields
class Base64BinaryField(serializers.CharField):
def to_representation(self, value):
# Encode the binary data to base64 string for JSON response
if value:
return base64.b64encode(value).decode("utf-8")
return None
def to_internal_value(self, data):
# Decode the base64 string to binary data when saving
try:
return base64.b64decode(data)
except (TypeError, ValueError):
raise serializers.ValidationError("Invalid base64-encoded data")
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
description_binary = Base64BinaryField()
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
"description_binary",
]
fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"]
read_only_fields = fields
@@ -818,51 +684,4 @@ class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
]
class IssueDescriptionVersionSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = [
"workspace",
"issue",
]
class IssueDescriptionVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"issue",
"last_saved_at",
"description_binary",
"description_html",
"description_json",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = [
"workspace",
"issue",
]
read_only_fields = ["workspace", "project", "issue"]
+7 -20
View File
@@ -21,10 +21,7 @@ from plane.db.models import (
class ModuleWriteSerializer(BaseSerializer):
lead_id = serializers.PrimaryKeyRelatedField(
source="lead",
queryset=User.objects.all(),
required=False,
allow_null=True,
source="lead", queryset=User.objects.all(), required=False, allow_null=True
)
member_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
@@ -48,9 +45,7 @@ class ModuleWriteSerializer(BaseSerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
data["member_ids"] = [
str(member.id) for member in instance.members.all()
]
data["member_ids"] = [str(member.id) for member in instance.members.all()]
return data
def validate(self, data):
@@ -59,9 +54,7 @@ class ModuleWriteSerializer(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):
@@ -71,9 +64,7 @@ class ModuleWriteSerializer(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=project
).exists():
if Module.objects.filter(name=module_name, project=project).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
@@ -104,9 +95,7 @@ class ModuleWriteSerializer(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()
):
@@ -203,8 +192,7 @@ class ModuleLinkSerializer(BaseSerializer):
def create(self, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url"))
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."})
return super().create(validated_data)
@@ -213,8 +201,7 @@ class ModuleLinkSerializer(BaseSerializer):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if (
ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=instance.module_id,
url=validated_data.get("url"), module_id=instance.module_id
)
.exclude(pk=instance.id)
.exists()
@@ -8,9 +8,7 @@ from rest_framework import serializers
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(
read_only=True, source="triggered_by"
)
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
is_inbox_issue = serializers.BooleanField(read_only=True)
is_intake_issue = serializers.BooleanField(read_only=True)
is_mentioned_notification = serializers.BooleanField(read_only=True)
+8 -32
View File
@@ -22,14 +22,8 @@ class PageSerializer(BaseSerializer):
required=False,
)
# Many to many
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
project_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
project_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
anchor = serializers.CharField(read_only=True)
class Meta:
@@ -56,11 +50,7 @@ class PageSerializer(BaseSerializer):
"project_ids",
"anchor",
]
read_only_fields = [
"workspace",
"owned_by",
"anchor",
]
read_only_fields = ["workspace", "owned_by", "anchor"]
def create(self, validated_data):
labels = validated_data.pop("labels", None)
@@ -131,9 +121,7 @@ class PageDetailSerializer(PageSerializer):
is_favorite = serializers.BooleanField(read_only=True)
class Meta(PageSerializer.Meta):
fields = PageSerializer.Meta.fields + [
"description_html",
]
fields = PageSerializer.Meta.fields + ["description_html"]
class SubPageSerializer(BaseSerializer):
@@ -142,10 +130,7 @@ class SubPageSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = [
"workspace",
"page",
]
read_only_fields = ["workspace", "page"]
def get_entity_details(self, obj):
entity_name = obj.entity_name
@@ -162,10 +147,7 @@ class PageLogSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = [
"workspace",
"page",
]
read_only_fields = ["workspace", "page"]
class PageVersionSerializer(BaseSerializer):
@@ -182,10 +164,7 @@ class PageVersionSerializer(BaseSerializer):
"created_by",
"updated_by",
]
read_only_fields = [
"workspace",
"page",
]
read_only_fields = ["workspace", "page"]
class PageVersionDetailSerializer(BaseSerializer):
@@ -205,7 +184,4 @@ class PageVersionDetailSerializer(BaseSerializer):
"created_by",
"updated_by",
]
read_only_fields = [
"workspace",
"page",
]
read_only_fields = ["workspace", "page"]
+7 -25
View File
@@ -4,10 +4,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import (
UserLiteSerializer,
UserAdminLiteSerializer,
)
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
Project,
ProjectMember,
@@ -18,32 +15,23 @@ from plane.db.models import (
class ProjectSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
class Meta:
model = Project
fields = "__all__"
read_only_fields = [
"workspace",
"deleted_at",
]
read_only_fields = ["workspace", "deleted_at"]
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"]
)
@@ -82,9 +70,7 @@ class ProjectSerializer(BaseSerializer):
return project
# If not same fail update
raise serializers.ValidationError(
detail="Project Identifier is already taken"
)
raise serializers.ValidationError(detail="Project Identifier is already taken")
class ProjectLiteSerializer(BaseSerializer):
@@ -221,8 +207,4 @@ class ProjectPublicMemberSerializer(BaseSerializer):
class Meta:
model = ProjectPublicMember
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"member",
]
read_only_fields = ["workspace", "project", "member"]
+2 -10
View File
@@ -19,19 +19,11 @@ class StateSerializer(BaseSerializer):
"description",
"sequence",
]
read_only_fields = [
"workspace",
"project",
]
read_only_fields = ["workspace", "project"]
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = [
"id",
"name",
"color",
"group",
]
fields = ["id", "name", "color", "group"]
read_only_fields = fields
+11 -43
View File
@@ -2,13 +2,7 @@
from rest_framework import serializers
# Module import
from plane.db.models import (
Account,
Profile,
User,
Workspace,
WorkspaceMemberInvite,
)
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
from .base import BaseSerializer
@@ -17,11 +11,7 @@ class UserSerializer(BaseSerializer):
class Meta:
model = User
# Exclude password field from the serializer
fields = [
field.name
for field in User._meta.fields
if field.name != "password"
]
fields = [field.name for field in User._meta.fields if field.name != "password"]
# Make all system fields and email read only
read_only_fields = [
"id",
@@ -56,7 +46,6 @@ class UserSerializer(BaseSerializer):
class UserMeSerializer(BaseSerializer):
class Meta:
model = User
fields = [
@@ -87,11 +76,7 @@ class UserMeSettingsSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"email",
"workspace",
]
fields = ["id", "email", "workspace"]
read_only_fields = fields
def get_workspace(self, obj):
@@ -128,8 +113,7 @@ class UserMeSettingsSerializer(BaseSerializer):
else:
fallback_workspace = (
Workspace.objects.filter(
workspace_member__member_id=obj.id,
workspace_member__is_active=True,
workspace_member__member_id=obj.id, workspace_member__is_active=True
)
.order_by("created_at")
.first()
@@ -138,14 +122,10 @@ class UserMeSettingsSerializer(BaseSerializer):
"last_workspace_id": None,
"last_workspace_slug": None,
"fallback_workspace_id": (
fallback_workspace.id
if fallback_workspace is not None
else None
fallback_workspace.id if fallback_workspace is not None else None
),
"fallback_workspace_slug": (
fallback_workspace.slug
if fallback_workspace is not None
else None
fallback_workspace.slug if fallback_workspace is not None else None
),
"invites": workspace_invites,
}
@@ -163,10 +143,7 @@ class UserLiteSerializer(BaseSerializer):
"is_bot",
"display_name",
]
read_only_fields = [
"id",
"is_bot",
]
read_only_fields = ["id", "is_bot"]
class UserAdminLiteSerializer(BaseSerializer):
@@ -183,10 +160,7 @@ class UserAdminLiteSerializer(BaseSerializer):
"email",
"last_login_medium",
]
read_only_fields = [
"id",
"is_bot",
]
read_only_fields = ["id", "is_bot"]
class ChangePasswordSerializer(serializers.Serializer):
@@ -207,9 +181,7 @@ class ChangePasswordSerializer(serializers.Serializer):
if data.get("new_password") != data.get("confirm_password"):
raise serializers.ValidationError(
{
"error": "Confirm password should be same as the new password."
}
{"error": "Confirm password should be same as the new password."}
)
return data
@@ -227,15 +199,11 @@ class ProfileSerializer(BaseSerializer):
class Meta:
model = Profile
fields = "__all__"
read_only_fields = [
"user",
]
read_only_fields = ["user"]
class AccountSerializer(BaseSerializer):
class Meta:
model = Account
fields = "__all__"
read_only_fields = [
"user",
]
read_only_fields = ["user"]
+4 -13
View File
@@ -56,13 +56,9 @@ class WebhookSerializer(DynamicBaseSerializer):
# Additional validation for multiple request domains and their subdomains
request = self.context.get("request")
disallowed_domains = [
"plane.so",
] # Add your disallowed domains here
disallowed_domains = ["plane.so"] # Add your disallowed domains here
if request:
request_host = request.get_host().split(":")[
0
] # Remove port if present
request_host = request.get_host().split(":")[0] # Remove port if present
disallowed_domains.append(request_host)
# Check if hostname is a subdomain or exact match of any disallowed domain
@@ -114,9 +110,7 @@ class WebhookSerializer(DynamicBaseSerializer):
# Additional validation for multiple request domains and their subdomains
request = self.context.get("request")
disallowed_domains = [
"plane.so",
] # Add your disallowed domains here
disallowed_domains = ["plane.so"] # Add your disallowed domains here
if request:
request_host = request.get_host().split(":")[
0
@@ -137,10 +131,7 @@ class WebhookSerializer(DynamicBaseSerializer):
class Meta:
model = Webhook
fields = "__all__"
read_only_fields = [
"workspace",
"secret_key",
]
read_only_fields = ["workspace", "secret_key"]
class WebhookLogSerializer(DynamicBaseSerializer):
+5 -19
View File
@@ -47,11 +47,7 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
fields = ["name", "slug", "id"]
read_only_fields = fields
@@ -103,9 +99,7 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(
read_only=True, source="members", many=True
)
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
@@ -142,9 +136,7 @@ class TeamSerializer(BaseSerializer):
members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete()
team_members = [
TeamMember(
member=member, team=instance, workspace=instance.workspace
)
TeamMember(member=member, team=instance, workspace=instance.workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
@@ -156,17 +148,11 @@ class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
fields = "__all__"
read_only_fields = [
"workspace",
"actor",
]
read_only_fields = ["workspace", "actor"]
class WorkspaceUserPropertiesSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"user",
]
read_only_fields = ["workspace", "user"]
+2 -10
View File
@@ -26,11 +26,7 @@ urlpatterns = [
FileAssetEndpoint.as_view(),
name="file-assets",
),
path(
"users/file-assets/",
UserAssetsEndpoint.as_view(),
name="user-file-assets",
),
path("users/file-assets/", UserAssetsEndpoint.as_view(), name="user-file-assets"),
path(
"users/file-assets/<str:asset_key>/",
UserAssetsEndpoint.as_view(),
@@ -38,11 +34,7 @@ urlpatterns = [
),
path(
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/restore/",
FileAssetViewSet.as_view(
{
"post": "restore",
}
),
FileAssetViewSet.as_view({"post": "restore"}),
name="file-assets-restore",
),
# V2 Endpoints
+4 -23
View File
@@ -18,12 +18,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
CycleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
CycleViewSet.as_view({"get": "list", "post": "create"}),
name="project-cycle",
),
path(
@@ -40,12 +35,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
CycleIssueViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-cycle",
),
path(
@@ -67,21 +57,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
CycleFavoriteViewSet.as_view({"get": "list", "post": "create"}),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
CycleFavoriteViewSet.as_view({"delete": "destroy"}),
name="user-favorite-cycle",
),
path(
+4 -22
View File
@@ -16,42 +16,24 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
BulkEstimatePointEndpoint.as_view({"get": "list", "post": "create"}),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
EstimatePointEndpoint.as_view(
{
"post": "create",
}
),
EstimatePointEndpoint.as_view({"post": "create"}),
name="estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
EstimatePointEndpoint.as_view(
{
"patch": "partial_update",
"delete": "destroy",
}
),
EstimatePointEndpoint.as_view({"patch": "partial_update", "delete": "destroy"}),
name="estimate-points",
),
]
+2 -6
View File
@@ -6,17 +6,13 @@ from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpo
urlpatterns = [
path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="unsplash",
),
path("unsplash/", UnsplashEndpoint.as_view(), name="unsplash"),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
path(
path(
"workspaces/<str:slug>/ai-assistant/",
WorkspaceGPTIntegrationEndpoint.as_view(),
name="importer",
+9 -58
View File
@@ -1,105 +1,56 @@
from django.urls import path
from plane.app.views import (
IntakeViewSet,
IntakeIssueViewSet,
)
from plane.app.views import IntakeViewSet, IntakeIssueViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/",
IntakeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IntakeViewSet.as_view({"get": "list", "post": "create"}),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/",
IntakeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
IntakeIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IntakeIssueViewSet.as_view({"get": "list", "post": "create"}),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
IntakeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IntakeViewSet.as_view({"get": "list", "post": "create"}),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
IntakeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
IntakeIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IntakeIssueViewSet.as_view({"get": "list", "post": "create"}),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/description/",
IntakeIssueViewSet.as_view(
{
"get": "retrieve_description",
"post": "update_description",
}
),
name="inbox-issue-description",
),
]
+14 -110
View File
@@ -23,7 +23,6 @@ from plane.app.views import (
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
)
urlpatterns = [
@@ -34,12 +33,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue",
),
path(
@@ -66,24 +60,9 @@ urlpatterns = [
),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/description/",
IssueViewSet.as_view(
{
"get": "retrieve_description",
"post": "update_description",
}
),
name="project-issue-description",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
LabelViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
LabelViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-labels",
),
path(
@@ -116,12 +95,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
IssueLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueLinkViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-links",
),
path(
@@ -174,12 +148,7 @@ urlpatterns = [
## IssueComments
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueCommentViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-comment",
),
path(
@@ -198,12 +167,7 @@ urlpatterns = [
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueSubscriberViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-subscribers",
),
path(
@@ -214,11 +178,7 @@ urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{
"get": "subscription_status",
"post": "subscribe",
"delete": "unsubscribe",
}
{"get": "subscription_status", "post": "subscribe", "delete": "unsubscribe"}
),
name="project-issue-subscribers",
),
@@ -226,42 +186,24 @@ urlpatterns = [
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueReactionViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionViewSet.as_view(
{
"delete": "destroy",
}
),
IssueReactionViewSet.as_view({"delete": "destroy"}),
name="project-issue-reactions",
),
## End Issue Reactions
# Comment Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
CommentReactionViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-comment-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionViewSet.as_view(
{
"delete": "destroy",
}
),
CommentReactionViewSet.as_view({"delete": "destroy"}),
name="project-issue-comment-reactions",
),
## End Comment Reactions
@@ -275,52 +217,26 @@ urlpatterns = [
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
IssueArchiveViewSet.as_view(
{
"get": "list",
}
),
IssueArchiveViewSet.as_view({"get": "list"}),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
IssueArchiveViewSet.as_view(
{
"get": "retrieve",
"post": "archive",
"delete": "unarchive",
}
{"get": "retrieve", "post": "archive", "delete": "unarchive"}
),
name="project-issue-archive-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/description/",
IssueArchiveViewSet.as_view(
{
"get": "retrieve_description",
}
),
name="archive-issue-description",
),
## End Issue Archives
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueRelationViewSet.as_view({"get": "list", "post": "create"}),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
IssueRelationViewSet.as_view(
{
"post": "remove_relation",
}
),
IssueRelationViewSet.as_view({"post": "remove_relation"}),
name="issue-relation",
),
## End Issue Relation
@@ -334,16 +250,4 @@ urlpatterns = [
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
# Issue Description versions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="issue-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="issue-versions",
),
## End Issue Description versions
]
+6 -34
View File
@@ -14,12 +14,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
ModuleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ModuleViewSet.as_view({"get": "list", "post": "create"}),
name="project-modules",
),
path(
@@ -36,21 +31,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
ModuleIssueViewSet.as_view(
{
"post": "create_issue_modules",
}
),
ModuleIssueViewSet.as_view({"post": "create_issue_modules"}),
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
"get": "list",
}
),
ModuleIssueViewSet.as_view({"post": "create_module_issues", "get": "list"}),
name="project-module-issues",
),
path(
@@ -67,12 +53,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
ModuleLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ModuleLinkViewSet.as_view({"get": "list", "post": "create"}),
name="project-issue-module-links",
),
path(
@@ -89,21 +70,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ModuleFavoriteViewSet.as_view({"get": "list", "post": "create"}),
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
ModuleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
ModuleFavoriteViewSet.as_view({"delete": "destroy"}),
name="user-favorite-module",
),
path(
+5 -27
View File
@@ -12,42 +12,24 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/users/notifications/",
NotificationViewSet.as_view(
{
"get": "list",
}
),
NotificationViewSet.as_view({"get": "list"}),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
NotificationViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
NotificationViewSet.as_view(
{
"post": "mark_read",
"delete": "mark_unread",
}
),
NotificationViewSet.as_view({"post": "mark_read", "delete": "mark_unread"}),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
NotificationViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
NotificationViewSet.as_view({"post": "archive", "delete": "unarchive"}),
name="notifications",
),
path(
@@ -57,11 +39,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/users/notifications/mark-all-read/",
MarkAllReadNotificationViewSet.as_view(
{
"post": "create",
}
),
MarkAllReadNotificationViewSet.as_view({"post": "create"}),
name="mark-all-read-notifications",
),
path(
+7 -40
View File
@@ -14,34 +14,20 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
PageViewSet.as_view({"get": "list", "post": "create"}),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-pages",
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view(
{
"post": "create",
"delete": "destroy",
}
),
PageFavoriteViewSet.as_view({"post": "create", "delete": "destroy"}),
name="user-favorite-pages",
),
# Lock
@@ -58,33 +44,19 @@ urlpatterns = [
# archived pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
PageViewSet.as_view({"post": "archive", "delete": "unarchive"}),
name="project-page-archive-unarchive",
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
PageViewSet.as_view(
{
"post": "lock",
"delete": "unlock",
}
),
PageViewSet.as_view({"post": "lock", "delete": "unlock"}),
name="project-pages-lock-unlock",
),
# private and public page
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/access/",
PageViewSet.as_view(
{
"post": "access",
}
),
PageViewSet.as_view({"post": "access"}),
name="project-pages-access",
),
path(
@@ -104,12 +76,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
PagesDescriptionViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
}
),
PagesDescriptionViewSet.as_view({"get": "retrieve", "patch": "partial_update"}),
name="page-description",
),
path(
+11 -62
View File
@@ -21,12 +21,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/",
ProjectViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ProjectViewSet.as_view({"get": "list", "post": "create"}),
name="project",
),
path(
@@ -48,32 +43,17 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
ProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
ProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
ProjectInvitationsViewset.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
ProjectInvitationsViewset.as_view({"get": "retrieve", "delete": "destroy"}),
name="project-member-invite",
),
path(
"users/me/workspaces/<str:slug>/projects/invitations/",
UserProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
name="user-project-invitations",
),
path(
@@ -88,32 +68,19 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
ProjectMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ProjectMemberViewSet.as_view({"get": "list", "post": "create"}),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
ProjectMemberViewSet.as_view(
{
"post": "leave",
}
),
ProjectMemberViewSet.as_view({"post": "leave"}),
name="project-member",
),
path(
@@ -133,21 +100,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
ProjectFavoritesViewSet.as_view({"get": "list", "post": "create"}),
name="project-favorite",
),
path(
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
}
),
ProjectFavoritesViewSet.as_view({"delete": "destroy"}),
name="project-favorite",
),
path(
@@ -157,22 +115,13 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
DeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
DeployBoardViewSet.as_view({"get": "list", "post": "create"}),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
DeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-deploy-board",
),
+3 -16
View File
@@ -7,32 +7,19 @@ from plane.app.views import StateViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
StateViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
StateViewSet.as_view({"get": "list", "post": "create"}),
name="project-states",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
StateViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-state",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/mark-default/",
StateViewSet.as_view(
{
"post": "mark_as_default",
}
),
StateViewSet.as_view({"post": "mark_as_default"}),
name="project-state",
),
]
+10 -44
View File
@@ -23,60 +23,30 @@ urlpatterns = [
path(
"users/me/",
UserEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "deactivate",
}
{"get": "retrieve", "patch": "partial_update", "delete": "deactivate"}
),
name="users",
),
path(
"users/session/",
UserSessionEndpoint.as_view(),
name="user-session",
),
path("users/session/", UserSessionEndpoint.as_view(), name="user-session"),
path(
"users/me/settings/",
UserEndpoint.as_view(
{
"get": "retrieve_user_settings",
}
),
UserEndpoint.as_view({"get": "retrieve_user_settings"}),
name="users",
),
# Profile
path(
"users/me/profile/",
ProfileEndpoint.as_view(),
name="accounts",
),
path("users/me/profile/", ProfileEndpoint.as_view(), name="accounts"),
# End profile
# Accounts
path(
"users/me/accounts/",
AccountEndpoint.as_view(),
name="accounts",
),
path(
"users/me/accounts/<uuid:pk>/",
AccountEndpoint.as_view(),
name="accounts",
),
path("users/me/accounts/", AccountEndpoint.as_view(), name="accounts"),
path("users/me/accounts/<uuid:pk>/", AccountEndpoint.as_view(), name="accounts"),
## End Accounts
path(
"users/me/instance-admin/",
UserEndpoint.as_view(
{
"get": "retrieve_instance_admin",
}
),
UserEndpoint.as_view({"get": "retrieve_instance_admin"}),
name="users",
),
path(
"users/me/onboard/",
UpdateUserOnBoardedEndpoint.as_view(),
name="user-onboard",
"users/me/onboard/", UpdateUserOnBoardedEndpoint.as_view(), name="user-onboard"
),
path(
"users/me/tour-completed/",
@@ -84,9 +54,7 @@ urlpatterns = [
name="user-tour",
),
path(
"users/me/activities/",
UserActivityEndpoint.as_view(),
name="user-activities",
"users/me/activities/", UserActivityEndpoint.as_view(), name="user-activities"
),
path(
"users/me/verify/",
@@ -95,9 +63,7 @@ urlpatterns = [
),
# user workspaces
path(
"users/me/workspaces/",
UserWorkSpacesEndpoint.as_view(),
name="user-workspace",
"users/me/workspaces/", UserWorkSpacesEndpoint.as_view(), name="user-workspace"
),
# User Graphs
path(
+5 -28
View File
@@ -12,12 +12,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
IssueViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueViewViewSet.as_view({"get": "list", "post": "create"}),
name="project-view",
),
path(
@@ -34,12 +29,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/views/",
WorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
WorkspaceViewViewSet.as_view({"get": "list", "post": "create"}),
name="global-view",
),
path(
@@ -56,30 +46,17 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/issues/",
WorkspaceViewIssuesViewSet.as_view(
{
"get": "list",
}
),
WorkspaceViewIssuesViewSet.as_view({"get": "list"}),
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
IssueViewFavoriteViewSet.as_view({"get": "list", "post": "create"}),
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
IssueViewFavoriteViewSet.as_view({"delete": "destroy"}),
name="user-favorite-view",
),
]
+1 -5
View File
@@ -8,11 +8,7 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/webhooks/",
WebhookEndpoint.as_view(),
name="webhooks",
),
path("workspaces/<str:slug>/webhooks/", WebhookEndpoint.as_view(), name="webhooks"),
path(
"workspaces/<str:slug>/webhooks/<uuid:pk>/",
WebhookEndpoint.as_view(),
+11 -71
View File
@@ -39,12 +39,7 @@ urlpatterns = [
),
path(
"workspaces/",
WorkSpaceViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
WorkSpaceViewSet.as_view({"get": "list", "post": "create"}),
name="workspace",
),
path(
@@ -61,34 +56,20 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/invitations/",
WorkspaceInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
WorkspaceInvitationsViewset.as_view({"get": "list", "post": "create"}),
name="workspace-invitations",
),
path(
"workspaces/<str:slug>/invitations/<uuid:pk>/",
WorkspaceInvitationsViewset.as_view(
{
"delete": "destroy",
"get": "retrieve",
"patch": "partial_update",
}
{"delete": "destroy", "get": "retrieve", "patch": "partial_update"}
),
name="workspace-invitations",
),
# user workspace invitations
path(
"users/me/workspaces/invitations/",
UserWorkspaceInvitationsViewSet.as_view(
{
"get": "list",
"post": "create",
},
),
UserWorkspaceInvitationsViewSet.as_view({"get": "list", "post": "create"}),
name="user-workspace-invitations",
),
path(
@@ -110,31 +91,18 @@ urlpatterns = [
path(
"workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view(
{
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
}
{"patch": "partial_update", "delete": "destroy", "get": "retrieve"}
),
name="workspace-member",
),
path(
"workspaces/<str:slug>/members/leave/",
WorkSpaceMemberViewSet.as_view(
{
"post": "leave",
},
),
WorkSpaceMemberViewSet.as_view({"post": "leave"}),
name="leave-workspace-members",
),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
TeamMemberViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-team-members",
),
path(
@@ -166,22 +134,13 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/workspace-themes/",
WorkspaceThemeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
WorkspaceThemeViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-themes",
),
path(
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
WorkspaceThemeViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-themes",
),
@@ -257,32 +216,13 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/draft-issues/",
WorkspaceDraftIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
WorkspaceDraftIssueViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-draft-issues",
),
path(
"workspaces/<str:slug>/draft-issues/<uuid:pk>/",
WorkspaceDraftIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace-drafts-issues",
),
path(
"workspaces/<str:slug>/draft-issues/<uuid:pk>/description/",
WorkspaceDraftIssueViewSet.as_view(
{
"get": "retrieve_description",
"post": "update_description",
}
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-drafts-issues",
),
+22 -75
View File
@@ -59,12 +59,8 @@ from .workspace.invite import (
WorkspaceJoinEndpoint,
UserWorkspaceInvitationsViewSet,
)
from .workspace.label import (
WorkspaceLabelsEndpoint,
)
from .workspace.state import (
WorkspaceStatesEndpoint,
)
from .workspace.label import WorkspaceLabelsEndpoint
from .workspace.state import WorkspaceStatesEndpoint
from .workspace.user import (
UserLastProjectWithWorkspaceEndpoint,
WorkspaceUserProfileIssuesEndpoint,
@@ -75,15 +71,9 @@ from .workspace.user import (
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
)
from .workspace.estimate import (
WorkspaceEstimatesEndpoint,
)
from .workspace.module import (
WorkspaceModulesEndpoint,
)
from .workspace.cycle import (
WorkspaceCyclesEndpoint,
)
from .workspace.estimate import WorkspaceEstimatesEndpoint
from .workspace.module import WorkspaceModulesEndpoint
from .workspace.cycle import WorkspaceCyclesEndpoint
from .state.base import StateViewSet
from .view.base import (
@@ -105,15 +95,8 @@ from .cycle.base import (
CycleIssueStateAnalyticsEndpoint,
)
from .cycle.issue import CycleIssueViewSet
from .cycle.archive import (
CycleArchiveUnarchiveEndpoint,
)
from .asset.base import (
FileAssetEndpoint,
UserAssetsEndpoint,
FileAssetViewSet,
)
from .cycle.archive import CycleArchiveUnarchiveEndpoint
from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .asset.v2 import (
WorkspaceFileAssetEndpoint,
UserAssetsV2Endpoint,
@@ -133,13 +116,9 @@ from .issue.base import (
IssueBulkUpdateDateEndpoint,
)
from .issue.activity import (
IssueActivityEndpoint,
)
from .issue.activity import IssueActivityEndpoint
from .issue.version import IssueVersionEndpoint
from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint
from .issue.archive import IssueArchiveViewSet
from .issue.attachment import (
IssueAttachmentEndpoint,
@@ -147,35 +126,19 @@ from .issue.attachment import (
IssueAttachmentV2Endpoint,
)
from .issue.comment import (
IssueCommentViewSet,
CommentReactionViewSet,
)
from .issue.comment import IssueCommentViewSet, CommentReactionViewSet
from .issue.label import (
LabelViewSet,
BulkCreateIssueLabelsEndpoint,
)
from .issue.label import LabelViewSet, BulkCreateIssueLabelsEndpoint
from .issue.link import (
IssueLinkViewSet,
)
from .issue.link import IssueLinkViewSet
from .issue.relation import (
IssueRelationViewSet,
)
from .issue.relation import IssueRelationViewSet
from .issue.reaction import (
IssueReactionViewSet,
)
from .issue.reaction import IssueReactionViewSet
from .issue.sub_issue import (
SubIssuesEndpoint,
)
from .issue.sub_issue import SubIssuesEndpoint
from .issue.subscriber import (
IssueSubscriberViewSet,
)
from .issue.subscriber import IssueSubscriberViewSet
from .module.base import (
ModuleViewSet,
@@ -184,18 +147,11 @@ from .module.base import (
ModuleUserPropertiesEndpoint,
)
from .module.issue import (
ModuleIssueViewSet,
)
from .module.issue import ModuleIssueViewSet
from .module.archive import (
ModuleArchiveUnarchiveEndpoint,
)
from .module.archive import ModuleArchiveUnarchiveEndpoint
from .api import (
ApiTokenEndpoint,
ServiceApiTokenEndpoint,
)
from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint
from .page.base import (
PageViewSet,
@@ -206,17 +162,11 @@ from .page.base import (
)
from .page.version import PageVersionEndpoint
from .search.base import (
GlobalSearchEndpoint,
SearchEndpoint,
)
from .search.base import GlobalSearchEndpoint, SearchEndpoint
from .search.base import GlobalSearchEndpoint
from .search.issue import IssueSearchEndpoint
from .search.workspace import (
WorkspaceSearchEndpoint,
WorkspaceEntitySearchEndpoint,
)
from .search.workspace import WorkspaceSearchEndpoint, WorkspaceEntitySearchEndpoint
from .external.base import (
GPTIntegrationEndpoint,
@@ -266,10 +216,7 @@ from .importer.base import (
BulkImportModulesEndpoint,
)
from .integration.base import (
IntegrationViewSet,
WorkspaceIntegrationViewSet,
)
from .integration.base import IntegrationViewSet, WorkspaceIntegrationViewSet
from .integration.github import (
GithubRepositoriesEndpoint,
+20 -78
View File
@@ -22,14 +22,7 @@ from plane.app.permissions import allow_permission, ROLE
class AnalyticsEndpoint(BaseAPIView):
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug):
x_axis = request.GET.get("x_axis", False)
y_axis = request.GET.get("y_axis", False)
@@ -50,10 +43,7 @@ class AnalyticsEndpoint(BaseAPIView):
"completed_at",
]
valid_yaxis = [
"issue_count",
"estimate",
]
valid_yaxis = ["issue_count", "estimate"]
# Check for x-axis and y-axis as thery are required parameters
if (
@@ -70,9 +60,7 @@ class AnalyticsEndpoint(BaseAPIView):
)
# If segment is present it cannot be same as x-axis
if segment and (
segment not in valid_xaxis_segment or x_axis == segment
):
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
return Response(
{
"error": "Both segment and x axis cannot be same and segment should be valid"
@@ -97,10 +85,7 @@ class AnalyticsEndpoint(BaseAPIView):
state_details = {}
if x_axis in ["state_id"] or segment in ["state_id"]:
state_details = (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
)
Issue.issue_objects.filter(workspace__slug=slug, **filters)
.distinct("state_id")
.order_by("state_id")
.values("state_id", "state__name", "state__color")
@@ -163,9 +148,7 @@ class AnalyticsEndpoint(BaseAPIView):
)
cycle_details = {}
if x_axis in ["issue_cycle__cycle_id"] or segment in [
"issue_cycle__cycle_id"
]:
if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
cycle_details = (
Issue.issue_objects.filter(
workspace__slug=slug,
@@ -175,10 +158,7 @@ class AnalyticsEndpoint(BaseAPIView):
)
.distinct("issue_cycle__cycle_id")
.order_by("issue_cycle__cycle_id")
.values(
"issue_cycle__cycle_id",
"issue_cycle__cycle__name",
)
.values("issue_cycle__cycle_id", "issue_cycle__cycle__name")
)
module_details = {}
@@ -194,10 +174,7 @@ class AnalyticsEndpoint(BaseAPIView):
)
.distinct("issue_module__module_id")
.order_by("issue_module__module_id")
.values(
"issue_module__module_id",
"issue_module__module__name",
)
.values("issue_module__module_id", "issue_module__module__name")
)
return Response(
@@ -217,9 +194,7 @@ class AnalyticsEndpoint(BaseAPIView):
class AnalyticViewViewset(BaseViewSet):
permission_classes = [
WorkSpaceAdminPermission,
]
permission_classes = [WorkSpaceAdminPermission]
model = AnalyticView
serializer_class = AnalyticViewSerializer
@@ -229,25 +204,14 @@ class AnalyticViewViewset(BaseViewSet):
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
)
class SavedAnalyticEndpoint(BaseAPIView):
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug, analytic_id):
analytic_view = AnalyticView.objects.get(
pk=analytic_id, workspace__slug=slug
)
analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
filter = analytic_view.query
queryset = Issue.issue_objects.filter(**filter)
@@ -273,14 +237,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
class ExportAnalyticsEndpoint(BaseAPIView):
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
x_axis = request.data.get("x_axis", False)
y_axis = request.data.get("y_axis", False)
@@ -301,10 +258,7 @@ class ExportAnalyticsEndpoint(BaseAPIView):
"completed_at",
]
valid_yaxis = [
"issue_count",
"estimate",
]
valid_yaxis = ["issue_count", "estimate"]
# Check for x-axis and y-axis as thery are required parameters
if (
@@ -321,9 +275,7 @@ class ExportAnalyticsEndpoint(BaseAPIView):
)
# If segment is present it cannot be same as x-axis
if segment and (
segment not in valid_xaxis_segment or x_axis == segment
):
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
return Response(
{
"error": "Both segment and x axis cannot be same and segment should be valid"
@@ -344,13 +296,10 @@ class ExportAnalyticsEndpoint(BaseAPIView):
class DefaultAnalyticsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
filters = issue_filters(request.GET, "GET")
base_issues = Issue.issue_objects.filter(
workspace__slug=slug, **filters
)
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
total_issues = base_issues.count()
@@ -363,9 +312,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
)
open_issues_groups = ["backlog", "unstarted", "started"]
open_issues_queryset = state_groups.filter(
state__group__in=open_issues_groups
)
open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
open_issues = open_issues_queryset.count()
open_issues_classified = (
@@ -407,8 +354,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
created_by__avatar_asset__isnull=True,
then="created_by__avatar",
created_by__avatar_asset__isnull=True, then="created_by__avatar"
),
default=Value(None),
output_field=models.CharField(),
@@ -441,8 +387,7 @@ class DefaultAnalyticsEndpoint(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(),
@@ -469,8 +414,7 @@ class DefaultAnalyticsEndpoint(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(),
@@ -479,9 +423,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
.order_by("-count")
)
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))[
"sum"
]
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))["sum"]
total_estimate_sum = base_issues.aggregate(sum=Sum("point"))["sum"]
return Response(

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