Compare commits

...
Author SHA1 Message Date
sriramveeraghanta 667fc7e3ab chore: update next path redirections with validation 2025-10-30 18:56:38 +05:30
350107d6c1 [WEB-5088] feat: Power K v2 (#7905)
* feat: add project shortcut in command palette

* feat: global project switcher shortcut

* refactor: generalize command palette entity handling

* feat: extend command palette navigation

* feat: add issue shortcut to command palette

* feat: add modular project selection for cycle navigation

* chore: add reusable command palette utilities

* fix: update key sequence handling to use window methods for timeout management

* fix: build errors

* chore: minor ux copy improvements

* feat: implement a new command registry and renderer for enhanced command palette functionality

* feat: introduce new command palette components and enhance search functionality

* feat: enhance command palette components with improved initialization and loading indicators

* feat: Implement new command palette architecture with multi-step commands, context-aware filtering, and reusable components. Add comprehensive documentation and integration guides. Enhance command execution with a dedicated executor and context provider. Introduce new command types and improve existing command definitions for better usability and maintainability.

* refactor: hook arguments

* refactor: folder structure

* refactor: update import paths

* fix: context prop drilling

* refactor: update search components

* refactor: create actions

* chore: add type to pages

* chore: init contextual actions

* refactor: context based actions code split

* chore: module context-based actions

* refactor: streamline command execution flow and enhance multi-step handling in command palette

* refactor: remove placeholder management from command execution and implement centralized placeholder mapping

* chore: cycle context based actions

* refactor: simplify command execution by consolidating selection steps and adding page change handling

* chore: added more options to work item contextual actions

* chore: page context actions

* refactor: update step type definitions and enhance page mapping for command execution

* feat: implement Command Palette V2 with global shortcuts and enhanced context handling

* refactor: power k v2

* refactor: creation commands

* feat: add navigation utility for Power K context handling

* feat: implement comprehensive navigation commands for Power K

* refactor: work item contextual actions

* fix: build errors

* refactor: remaining contextual actions

* refactor: remove old code

* chore: update placeholder

* refactor: enhance command registry with observable properties and context-aware shortcut handling

* refactor: improve command filtering logic in CommandPaletteModal

* chore: context indicator

* chore: misc actions

* style: shortcut badge

* feat: add open entity actions and enhance navigation commands for Power K

* refactor: rename and reorganize Power K components for improved clarity and structure

* refactor: update CommandPalette components and streamline global shortcuts handling

* refactor: adjust debounce timing in CommandPaletteModal for improved responsiveness

* feat: implement shortcuts modal and enhance command registry for better shortcut management

* fix: search implemented

* refactor: search results code split

* refactor: search results code split

* feat: introduce creation and navigation command modules for Power K, enhancing command organization and functionality

* chore: update menu logos

* refactor: remove unused PowerKOpenEntityActionsExtended component from command palette

* refactor: search menu

* fix: clear context on backspace and manual clear

* refactor: rename creation command keys for consistency and clarity in Power K

* chore: added intake in global search

* chore: preferences menu

* chore: removed the empty serach params

* revert: command palette changes

* cleanup

* refactor: update command IDs to use underscores for consistency across Power K components

* refactor: extended context based actions

* chore: modal command item status props

* refactor: replace CommandPalette with CommandPaletteProvider in settings and profile layouts

* refactor: update settings menu to use translated labels instead of i18n labels

* refactor: update command titles to use translation keys for creation actions

* refactor: update navigation command titles to use translation keys for consistency

* chore: minor cleanup

* chore: misc commands added

* chore: code split for no search results command

* chore: state menu items for work item context based commands

* chore: add more props to no search results command

* chore: add more props to no search results command

* refactor: remove shortcut key for create workspace command

* Refactor command palette to use PowerK store

- Replaced instances of `useCommandPalette` with `usePowerK` across various components, including `AppSearch`, `CommandModal`, and `CommandPalette`.
- Introduced `PowerKStore` to manage modal states and commands, enhancing the command palette functionality.
- Updated modal handling to toggle `PowerKModal` and `ShortcutsListModal` instead of the previous command palette modals.
- Refactored related components to ensure compatibility with the new store structure and maintain functionality.

* Refactor PowerK command handling to remove context dependency

- Updated `usePowerKCommands` and `usePowerKCreationCommands` to eliminate the need for a context parameter, simplifying their usage.
- Adjusted related command records to utilize the new structure, ensuring consistent access to command configurations.
- Enhanced permission checks in creation commands to utilize user project roles for better access control.

* chore: add context indicator

* chore: update type import

* chore: migrate toast implementation from @plane/ui to @plane/propel/toast across multiple command files

* refactor: power k modal wrapper and provider

* fix: type imports

* chore: update creation command shortcuts

* fix: page context commands

* chore: update navigation and open command shortcuts

* fix: work item standalone page modals

* fix: context indicator visibility

* fix: potential error points

* fix: build errors

* fix: lint errors

* fix: import order

---------

Co-authored-by: Vihar Kurama <vihar.kurama@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-10-29 15:38:30 +05:30
Jayash TripathyandGitHub 73e0e8d529 [WEB-4944] feat: add base layouts for kanban and list with drag-and-drop support (#8032) 2025-10-29 14:49:34 +05:30
Anmol Singh BhatiaandGitHub 5247fedd23 [WEB-5271] chore: your work empty state updated #8029 2025-10-29 13:34:04 +05:30
Anmol Singh BhatiaandGitHub 0560849f88 [WEB-5271] fix: empty state translation error and code refactoring (#8027)
* fix: workspace view empty state translation

* chore: search empty state translation fix

* chore: workspace view empty state translation
2025-10-28 20:14:07 +05:30
sriram veeraghantaandGitHub 79537cd1df chore: code splitting instance config variables (#8028)
* chore: code splitting instance config variables

* chore: typo fixes
2025-10-28 19:52:23 +05:30
1126ca30b0 [WEB-5262] feat: gitea sso (#8022)
* Feature/7137/gitea sso (#7940)

* added gitea auth to admin panel with configs , added api calls

* added gitea to oauth root (for signup and signin)

* removed log

* replace github oauth with gitea ouath error messages

* added gitea to auth root

* fix: update token expiration handling and remove unused variable in Gitea callback

* fix: include Gitea in OAuth enabled checks

* fix: improve error handling when fetching emails from Gitea

* chore : remove logs and add semicolons

* refactor: update Gitea authentication components and imports for consistency

* fix: enhance Gitea authentication form to auto-populate host value and improve OAuth checks

* refactor: enhance Gitea OAuth provider with improved error handling and URL validation

* fix: update authentication success messages to check for string value "1"

---------

Co-authored-by: Shivam Jain <shivam.clgstash@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-10-28 18:53:54 +05:30
SangeethaandGitHub 69fe581fd8 [WEB-5228] chore: IssueLink.DoesNotExist on crawl_work_item_link_title (#8003)
* chore: wrap IssueLink logic in a try-except block

* chore: user logger.warning

* fix: add id and url on the error message
2025-10-28 18:18:36 +05:30
Bavisetti NarayanandGitHub e09d986497 [WEB-5263] chore: removed the tracking of sanitized HTML (#8023)
* chore: removed the html sanitized tracking

* chore: added the greater than sign
2025-10-28 16:57:07 +05:30
SangeethaandGitHub 044003e7ec [WEB-5211] fix: accessing NoneType current_instance for delete_module_issue_activity bgtask (#7993)
* fix: accessing NoneType current_instance

* fix: return module_name

* fix: add none check
2025-10-28 16:56:39 +05:30
SangeethaandGitHub a8b6930486 [WEB-5224] chore: raise warning instead of logging an exception for webhook_send_task (#7996) 2025-10-28 16:49:19 +05:30
Anmol Singh BhatiaandGitHub 685e3fa2f8 [WEB-5236] chore: attachment icon revamp (#8009)
* chore: iconwrapper component added

* chore: attachments icon added to propel

* chore: code refactor

* chore: code refactor
2025-10-28 16:47:43 +05:30
Anmol Singh BhatiaandGitHub ec6e682044 [WEB-5255] fix: calendar layout weekly view (#8018)
* fix: calendar layout weekly view

* chore: code refactor
2025-10-28 16:47:10 +05:30
Anmol Singh BhatiaandGitHub b7aa25f2c6 [WEB-5248] chore: translations and empty state refactor (#8021)
* chore: translations code refactoring

* chore: empty state translation refactor

* chore: code refactor
2025-10-28 13:31:06 +05:30
Anmol Singh BhatiaandGitHub cf7f891bcb [WEB-5248] chore: empty state code refactor and translation fix (#8017)
* chore: empty state code refactor and translation fix

* chore: code refactor
2025-10-27 19:55:46 +05:30
Anmol Singh BhatiaandGitHub 3faf768112 [WEB-3567] chore: empty state refactoring and translation fix (#8014)
* chore: empty state component improvement and code refactor

* chore: translation code refactor

* chore: empty state code refactor
2025-10-27 17:03:35 +05:30
Vamsi KrishnaandGitHub be0d1871f0 [WEB-5241] fix: project features toggle #8015 2025-10-27 17:02:22 +05:30
Surya PrashanthandGitHub c07f7b7c1e [WEB-5245] fix: add is_time_tracking_enabled field in project serializer (#8016) 2025-10-27 16:54:03 +05:30
Surya PrashanthandGitHub 1d4cde9ba0 [WEB-5245] feat: auto-populate logo_props in project creation #8013 2025-10-27 16:19:52 +05:30
NikhilandGitHub c4dd4bd02f [WEB-5155]refactor: simplify user filtering logic in SearchEndpoint #8012 2025-10-27 16:17:19 +05:30
Anmol Singh BhatiaandGitHub d71dfe8f86 [WEB-5188 | WEB-5190] chore: layout and properties icon revamp (#7980) 2025-10-24 19:53:36 +05:30
Anmol Singh BhatiaandGitHub 33b6405695 [WEB-5160] chore: propel banner and archived work item improvements (#7999) 2025-10-24 19:48:28 +05:30
Anmol Singh BhatiaandGitHub 68fd2463f4 [WEB-5230 | WEB-5231] chore: new empty state implementation (#7972) 2025-10-24 17:21:14 +05:30
Vihar KuramaandGitHub a60d74a3c0 [WEB-5235] Fix activity timeline ordering for edited comments #8002 2025-10-24 16:17:51 +05:30
Anmol Singh BhatiaandGitHub 76ffe52cd5 [WEB-5220] fix: intake actions btn ui #7995 2025-10-24 16:05:46 +05:30
b65b1b4828 fix: use issue store was not loading up properly (#8006)
* use issue store was not loading up properly , and hence created data issues for rendering filters options

* Added React.FC for better type suggestions

* chore: observer implementation change

---------

Co-authored-by: shivam-jainn <shivam.clgstash@gmail.com>
2025-10-23 21:45:25 +05:30
sriram veeraghantaandGitHub e5063cd280 fix: get work item activity external endpoint (#8005)
* fix: activity get endpoint fixes

* chore: missing pk added
2025-10-23 21:37:30 +05:30
sriramveeraghanta 9448f642f0 sync: master branch with preview 2025-10-23 20:29:18 +05:30
Akshat Jainandsriramveeraghanta 443f8b793d [INFRA-252] fix: Add missing Redis environment variables in live service Docker Compose 2025-10-23 20:28:39 +05:30
Akshat JainandGitHub 96fa9ab15b [INFRA-252] fix: Add missing Redis environment variables in live service Docker Compose 2025-10-23 19:26:27 +05:30
sriram veeraghantaandGitHub d5bad5aedc release: v1.1.0 #7991 2025-10-23 18:24:42 +05:30
e710f5b278 [WEB-5196] chore: switch from isomorphic-dompurify to dompurify (#7983)
* [WEB-5196] chore: switch from isomorphic-dompurify to dompurify

Replace isomorphic-dompurify with dompurify package in utils.
This change simplifies the dependency and uses the canonical
DOMPurify package directly.

* fix: removing dompurify from the space app as dependency

* chore: remove unused import

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-10-23 16:33:32 +05:30
sriramveeraghanta 80bd1b6dcd Merge branch 'canary' of github.com:makeplane/plane into preview 2025-10-23 15:43:16 +05:30
b34c9ca04f fix: live server secret key app version env variables (#7997)
* fix: live server secret key app version env variables

* fix: revert to stable version

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 15:42:43 +05:30
sriramveeraghanta ad16ea1440 Merge branch 'canary' of github.com:makeplane/plane into preview 2025-10-23 14:54:09 +05:30
sriramveeraghanta addf3c4104 chore: added live server secret key in community docker file 2025-10-23 14:53:41 +05:30
Anmol Singh BhatiaandGitHub 38cdf756a5 [WEB-5158] chore: calendar start of week alignment fix and code refactoring #7978 2025-10-23 13:59:29 +05:30
Bavisetti NarayanandGitHub f94da68597 [WEB-5153] chore: optimised the cycle transfer issues (#7969)
* chore: optimised the cycle transfer issues

* chore: added more vlaidation in transfer

* chore: improve the comments
2025-10-23 00:29:32 +05:30
Bavisetti NarayanandGitHub 68aa2fe0b8 [WIKI-553] chore: improved pages components tracking (#7966)
* chore: page components tracking

* chore: changed the transaction task

* chore: added logger for description html
2025-10-23 00:29:05 +05:30
Vamsi KrishnaandGitHub 5fa9943b66 [WEB-5141] chore: project features refactor (#7960)
* chore: project features refactor

* fix: enable/disable condition for toggle

* fix: dependencies

* chore: lint fix

* chore: lint fix

* chore: added fallback exports
2025-10-23 00:27:16 +05:30
Anmol Singh BhatiaandGitHub 772b5c5719 [WEB-5201] chore: work item and inbox modal refactor (#7990)
* chore: work item and inbbox modal refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-10-22 21:11:08 +05:30
NikhilandGitHub d6657d5168 [WEB-5207] fix: update date formatting in IssueExportSchema for cycle start and end dates #7992 2025-10-22 17:11:43 +05:30
sriramveeraghanta 1b43efbc2a chore: version bump 2025-10-21 19:14:43 +05:30
64781be7d2 regression: downgrade to tiptap v2 (#7982)
* chore: downgrade to tiptap v2

* fix: revert back to hocuspocus

* fix: collaboration events added

* fix: lock unlock issues

* fix: build errors

* fix: type errors

* fix: graceful shutdown

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
2025-10-21 18:28:16 +05:30
sriramveeraghanta 59022b6beb fix: vite package upgrade 2025-10-21 14:41:34 +05:30
NikhilandGitHub f266cd8414 [WEB-5162] fix: add exporter URL patterns for exporting issues (#7977)
* feat: add exporter URL patterns for exporting issues and update session cookie age handling

* fix: ensure ADMIN_SESSION_COOKIE_AGE is an integer for consistent session handling
2025-10-16 15:36:48 +05:30
sriramveeraghanta 98b81d7ebb chore(deps): happy dom version bump 2025-10-16 14:55:57 +05:30
AaronandGitHub 00807d2d62 [WEB-5157] chore: fix propel icon clip paths #7975 2025-10-16 14:03:26 +05:30
Vamsi KrishnaandGitHub 9d757ccc6a [WEB-5156] fix: activity filters bug #7971 2025-10-15 19:57:52 +05:30
Anmol Singh BhatiaandGitHub a8c253acfe [WEB-5154] chore: nav button text colour updated #7970 2025-10-15 19:27:10 +05:30
Vamsi KrishnaandGitHub 2b106cbd66 [WEB-5054]feat: added activity filters for state and assignee activities (#7918)
* feat: added activity filters for state and assignee

* chore: removed unused funtion

* chore: lint fix
2025-10-15 17:12:03 +05:30
sriramveeraghanta f9cca8e2cb fix: live server log 2025-10-15 16:18:42 +05:30
ee176efae3 [WEB-5095]chore: updated delete modal info content (#7967)
* chore: updated delete modal info content

* chore: added language support for modal content

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-10-15 16:05:55 +05:30
Akshita GoyalandGitHub 44a483f895 [WEB-5142] fix: Issue modal styling #7961 2025-10-15 15:19:40 +05:30
sriramveeraghanta 196baf099a chore: lint errors 2025-10-15 15:15:48 +05:30
NikhilandGitHub 1a9ebc8b68 [WEB-5071] chore: implement webhook logging to MongoDB and fallback to database (#7896)
* feat: implement webhook logging to MongoDB and fallback to database (#7887)

- Added a new function `save_webhook_log` to log webhook requests and responses to MongoDB, with a fallback to the database if the MongoDB save fails.
- Updated the `webhook_send_task` to utilize the new logging function.
- Modified the `get_webhook_logs_queryset` to order logs by creation date and adjusted the chunk size for iteration.

* refactor: clean up log data formatting in save_webhook_log function

* fix: update retry_count type in save_webhook_log function
2025-10-15 14:53:43 +05:30
Vamsi KrishnaandGitHub 97e662215a [WEB-5125] fix: suspended user view #7964 2025-10-14 17:35:39 +05:30
Bavisetti NarayanandGitHub 606e34ec81 [WIKI-730] chore: handle body too large error (#7963)
* chore: added middleware to handle body too large

* chore: added middleware to handle body too large

* chore: indentend the code

* chore: changed the response structure

* chore: changed the response structure

* chore: created a new file for middleware

* chore: added a standardized error key
2025-10-14 17:21:11 +05:30
Anmol Singh BhatiaandGitHub a3019ebd46 [WEB-5092] feat: app sidebar enhancements (#7946)
* chore: project sidebar and settings sidebar improvement

* chore: navigationitem ui improvement

* chore: workspace level new icon added

* chore: propel icon migration

* chore: code refactor

* chore: dashboard icon updated

* chore: code refactor

* chore: icons updated

* chore: code refactor

* chore: code refactor

* chore: scroll area component refactor

* chore: sidebar enhancements

* chore: add and archive icon updated

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-10-14 17:20:20 +05:30
Prateek ShouryaandGitHub 9cfde896b3 [WEB-5134] refactor: update web ESLint configuration and refactor imports to use type imports (#7957)
* [WEB-5134] refactor: update `web` ESLint configuration and refactor imports to use type imports

- Enhanced ESLint configuration by adding new rules for import consistency and type imports.
- Refactored multiple files to replace regular imports with type imports for better clarity and performance.
- Ensured consistent use of type imports across the application to align with TypeScript best practices.

* refactor: standardize type imports across components

- Updated multiple files to replace regular imports with type imports for improved clarity and consistency.
- Ensured adherence to TypeScript best practices in the rich filters and issue layouts components.
2025-10-14 16:45:07 +05:30
Dheeraj Kumar KetireddyandGitHub 4168127803 [WEB-4999] feat: implement flexible data export utility with CSV, JSON, and XLSX support (#7884)
* feat: implement flexible data export utility with CSV, JSON, and XLSX support

- Introduced Exporter class for handling various data formats.
- Added formatters for CSV, JSON, and XLSX exports.
- Created schemas for defining export fields and their transformations.
- Implemented IssueExportSchema for exporting issue data with nested attributes.
- Enhanced issue export task to utilize the new exporter system for better data handling.

* feat: enhance issue export functionality with new relations and context handling

- Updated issue export task to utilize new IssueRelation model for better relationship management.
- Refactored Exporter class to accept QuerySets directly, improving performance and flexibility.
- Enhanced IssueExportSchema to include parent issues and relations in the export.
- Improved documentation for exporting multiple projects and filtering fields during export.

* feat: enhance export functionality with field filtering and context support

- Updated Exporter class to merge fields into options for formatting.
- Modified formatters to filter fields based on specified options.
- Enhanced ExportSchema to support optional field selection during serialization.
- Improved documentation for the serialize method to clarify field filtering capabilities.

* fixed type
2025-10-14 15:46:55 +05:30
Anmol Singh BhatiaandGitHub 9dc14d8d67 [WEB-5139] fix: custom menu item link (#7959)
* fix: custom menu link action

* chore: code refactor

* chore: code refactor
2025-10-14 15:45:49 +05:30
Anmol Singh BhatiaandGitHub 56007e7d47 [WEB-5129] chore: icons revamp (#7958)
* chore: code refactor

* chore: code refactor
2025-10-14 12:47:53 +05:30
Prateek ShouryaandGitHub cfb4a8212c [WEB-5099] improvement: enhance rich filters with new components and configurations (#7916)
* feat: enhance rich filters with new components and configurations

- Added `AdditionalFilterValueInput` for unsupported filter types.
- Introduced `FilterItem` and related components for better filter item management.
- Updated filter configurations to include new properties and support for multiple values.
- Improved loading states and error handling in filter components.
- Refactored existing filter logic to streamline operations and enhance performance.

* Refactor rich filters component structure and enhance filter item functionality

- Moved AddFilterButton and AddFilterDropdown to a new directory structure for better organization.
- Updated FilterItemProperty to handle filter selection and condition updates more effectively.
- Enhanced the FilterInstance class with methods to update condition properties and operators, improving filter management.
- Added new functionality to handle invalid filter states and improve user feedback.

* [WEB-5111] feat: add 'created_at' and 'updated_at' filters to work item configuration

- Introduced new filter configurations for 'created_at' and 'updated_at' in the work item filters.
- Updated relevant components to utilize these new filters, enhancing filtering capabilities.
- Added corresponding filter configuration functions in the utils for better date handling.

* fix: build
2025-10-14 01:39:24 +05:30
9f41e92d21 [WEB-5135] refactor: update sites ESLint configuration and refactor imports to use type imports (#7956)
- Enhanced ESLint configuration by adding new rules for import consistency and type imports.
- Refactored multiple files to replace regular imports with type imports for better clarity and performance.
- Ensured consistent use of type imports across the application to align with TypeScript best practices.

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-10-14 00:40:30 +05:30
Vamsi KrishnaandGitHub ddf07dc993 [WEB-5029] chore: updates icons for title and description #7862 2025-10-14 00:09:50 +05:30
eee9744159 [WEB-5119] chore: remove any deprecated toast instances from the plane/ui page (#7943)
* [WEB-5119] chore: remove deprecated toast implementation from @plane/ui

* fix: build

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-10-14 00:08:00 +05:30
Prateek ShouryaandGitHub c80c76b882 [WEB-5136] refactor: update admin ESLint configuration and refactor imports to use type imports (#7955)
- Enhanced ESLint configuration by adding new rules for import consistency and type imports.
- Refactored multiple files to replace regular imports with type imports for better clarity and performance.
- Ensured consistent use of type imports across the application to align with TypeScript best practices.
2025-10-13 21:07:49 +05:30
Vamsi KrishnaandGitHub ffe38b592a [WEB-5080] fix: attachment and link count in spreadsheet columns #7942 2025-10-13 21:03:54 +05:30
Anmol Singh BhatiaandGitHub 87eb1949c6 [WEB-5129] feat: icons revamp (#7945) 2025-10-13 19:43:24 +05:30
Anmol Singh BhatiaandGitHub c736354739 [WEB-5130] chore: propel scroll area improvement #7952 2025-10-13 19:30:58 +05:30
sriramveeraghanta 3eecec552a chore(deps): lock file resolutions 2025-10-13 13:43:05 +05:30
sriramveeraghanta c561164d65 chore(deps): happy dom resolution added 2025-10-13 13:32:13 +05:30
c3dd790f7e [WEB-5058] feat: enhance workspace seeding with cycles, modules, and views creation (#7951)
* feat: enhance workspace seeding with cycles, modules, and views creation

- Added `create_cycles`, `create_modules`, and `create_views` functions to the workspace seeding process, enabling the creation of cycles, modules, and views based on new seed data.
- Updated `create_project_issues` to associate issues with cycles and modules.
- Introduced new seed files: `cycles.json`, `modules.json`, and `views.json` to provide initial data for cycles, modules, and views.
- Integrated these new functionalities into the `workspace_seed` task for comprehensive workspace initialization.

* feat: add project_id to page seed data for improved association

- Added `project_id` field to the page with `id` 2 in `pages.json` to establish a clear link between pages and their respective projects.
- This enhancement supports better organization and retrieval of page data within the project context.

* feat: enhance workspace seed task with improved display properties and layout options

- Updated the `create_project_and_member` function to include new display properties and layout configurations for better project visualization.
- Modified display filters to group by state and added calendar layout options.
- Enhanced the `create_modules` and `create_views` functions with improved formatting and structure for better readability and maintainability.

* Update apps/api/plane/bgtasks/workspace_seed_task.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: correct project_id mapping in cycle and view creation functions

- Updated the `create_cycles` function to use the correct `project_id` from the `project_map` for fetching the last cycle.
- Removed redundant `view_id` extraction in the `create_views` function to streamline view creation process.

* refactor: update create_cycles function to return project mapping

- Changed the return type of the `create_cycles` function from `None` to `Dict[int, uuid.UUID]` to provide a mapping of project IDs after cycle creation.
- This modification enhances the function's utility by allowing the caller to access the generated project mappings directly.

* refactor: remove unused view_map variable in create_views function

- Eliminated the `view_map` dictionary from the `create_views` function as it was not utilized, streamlining the code.
- This change enhances code clarity and maintainability by removing unnecessary elements.

* refactor: improve issue creation logic in create_project_issues function

- Added comments to clarify the creation of issue labels, cycle issues, and module issues within the `create_project_issues` function.
- Enhanced code readability and maintainability by structuring the issue creation process with clear conditional checks for cycles and modules.

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-11 20:55:48 +05:30
Akshita GoyalandGitHub 70be4a4ace [WEB-5127] chore: added readonly properties + fixed state dropdown + added additional props to issue modal (#7949)
* chore: added readonly properties + fixed state dropdown + added addditional props to issue modal

* fix: refactor

* fix: refactors

* fix: build
2025-10-11 16:18:32 +05:30
Anmol Singh BhatiaandGitHub 2c17f8ad72 [WEB-5103] fix: custom menu event propagation (#7932)
* fix: custommenu event propagation

* chore: code refactor
2025-10-11 15:02:34 +05:30
NikhilandGitHub 151674687c [WEB-5059] feat: add page creation functionality to workspace seeding process (#7948)
* feat: add page creation functionality to workspace seeding process

- Implemented `create_pages` function to create pages for each project in the workspace based on data from `pages.json`.
- Integrated page creation into the `workspace_seed` task, ensuring pages are created alongside project issues.
- Added a new `pages.json` seed file containing initial page data and descriptions.

* fix: update page creation logic and seed data

- Set `is_global` to `False` for pages created in the `create_pages` function.
- Adjusted the project type check to be case-insensitive in the page creation logic.
- Added `id`, `project_id`, and `description_stripped` fields to the `pages.json` seed data for improved page initialization.
2025-10-10 18:32:15 +05:30
sriram veeraghantaandGitHub 8cd29c5009 fix: adding comprehensive logs for live server (#7947)
* fix: adding comprehensive logs

* fix: document controller naming convention

* fix: axios interception logger
2025-10-10 18:28:52 +05:30
Jayash TripathyandGitHub 9ce6179421 [WEB-5120] refactor: add onSearchQueryKeyDown prop to ComboboxOptions for keyboard event handling #7941 2025-10-10 16:46:47 +05:30
sriram veeraghanta 71ec9fadb7 chore: package versions 2025-10-10 01:52:23 +05:30
sriram veeraghanta c88ed332e5 chore: package version upgrade 2025-10-10 01:36:32 +05:30
Aaryan KhandelwalandGitHub d8c1dff34f [WIKI-725] regression: editor floaters propagation #7938 2025-10-10 01:35:14 +05:30
Akshat JainandGitHub 5d161f671d [INFRA-246] Update Valkey image version to 7.2.11-alpine in docker-compose files #7935 2025-10-10 00:19:17 +05:30
sriram veeraghantaandGitHub ae215a542e [WIKI-724 ] feat: sentry setup for live express server #7934 2025-10-09 21:00:43 +05:30
Anmol Singh BhatiaandGitHub b7c14ac9f5 [WEB-5104] chore: combobox improvements (#7933)
* chore: base ui package upgrade

* feat: migrate combobox to use base-ui primitives

* chore: code refactor

* chore: code refactor
2025-10-09 19:13:17 +05:30
75cd2017a0 [WEB-5104] chore: base-ui package upgrade and combobox migration (#7931)
* chore: base ui package upgrade

* feat: migrate combobox to use base-ui primitives

* chore: code refactor

* fix: format fixes

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-10-09 18:47:09 +05:30
Rohan BirtiaandGitHub 1895cfe728 Update docker-compose.yml for valkey security patch (#7926)
Critical security patch for Redis.
https://github.com/valkey-io/valkey/security/advisories/GHSA-9rfg-jx7v-52p6
2025-10-09 17:15:49 +05:30
M. PalanikannanandGitHub 98e0089723 [WIKI-722] fix: hocuspocus provider update to v3 (#7930)
* fix: hocuspocus provider

* fix: hocuspocus updated url instead of queryparams
2025-10-09 15:38:15 +05:30
sriramveeraghanta 561ae82d9d chore: update gitignore and dockerignore 2025-10-09 15:35:11 +05:30
Vamsi KrishnaandGitHub afac9f72db [WEB-5101]fix: toast action items hover behaviour (#7929)
* fix: toast action items hover behaviour

* fix: refactored toast styles
2025-10-09 15:10:56 +05:30
M. PalanikannanandGitHub 0f038705ed fix: hocuspocus version upgrade (#7928) 2025-10-09 14:32:43 +05:30
Prateek ShouryaandGitHub 0b257c8693 [WEB-5093] improvement: adds content-based MIME type detection for uploads #7925
- Refactored file upload utilities to use async functions for better handling of file metadata.
- Introduced MIME type detection using the file-type library.
- Updated file service methods to await metadata retrieval.
- Added new dependencies for file-type and updated package.json accordingly.
- Removed deprecated file handling code from utils and adjusted imports across services.
2025-10-09 12:22:43 +05:30
Aaryan KhandelwalandGitHub f2539c5051 fix: editor dropdowns positioning (#7927)
* fix: editor dropdowns positioning

* fix: add cleanup to prevent memory leak

* chore: add editor fallback
2025-10-09 00:25:21 +05:30
sriram veeraghantaandGitHub 5d60d6d702 chore: adding types node package to pnpm catalog (#7923) 2025-10-08 18:42:53 +05:30
3cbb60417c [WEB-4976]chore: propel toast implementation (#7832)
* draft: implementation

* chore: migrated toast from @plane/ui to propel package

* chore: reverted unwanted changes

* fix: lint errors

* chore: added dismiss toast to propel toast

* fix: build and lint errors

* chore: code refactor

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2025-10-07 23:37:10 +05:30
Anmol Singh BhatiaandGitHub d38f4811b6 [WEB-3567] dev: empty state v2 (#7910) 2025-10-07 20:53:49 +05:30
Anmol Singh BhatiaandGitHub 3e82984f92 [WEB-5068] fix: CustomMenu closeOnSelect behavior #7897 2025-10-07 20:52:30 +05:30
SangeethaandGitHub 89ddd1bf42 [WEB-5081] fix: update timezone in user settings #7915 2025-10-07 20:51:44 +05:30
NikhilandGitHub 0b15a32ec6 [WEB-5038] fix: cycle creation in external api endpoint (#7866)
* feat: set default owner for cycle creation if not provided

* Updated CycleListCreateAPIEndpoint to assign the current user as the owner when the 'owned_by' field is not included in the request data.
* Enhanced the CycleCreateSerializer initialization to ensure proper ownership assignment during cycle creation.

* feat: add comprehensive tests for Cycle API endpoints

* Introduced a new test suite for Cycle API endpoints, covering creation, retrieval, updating, and deletion of cycles.
* Implemented tests for various scenarios including successful operations, invalid data handling, and conflict resolution with external IDs.
* Enhanced test coverage for listing cycles with different view filters and verifying cycle metrics annotations.

* feat: enhance CycleCreateSerializer to include ownership assignment

* Added 'owned_by' field to CycleCreateSerializer to specify the user who owns the cycle.
* Updated CycleListCreateAPIEndpoint to remove redundant ownership assignment logic, relying on the serializer to handle default ownership.
* Ensured that if 'owned_by' is not provided, it defaults to the current user during cycle creation.

* fix: correct assertion syntax in CycleListCreateAPIEndpoint tests

* Updated the assertion in the test for successful cycle creation to use the correct syntax for checking the response status code.
* Ensured that the test accurately verifies the expected behavior of the API endpoint.
2025-10-07 18:50:09 +05:30
Anmol Singh BhatiaandGitHub f5eb13f66f [WEB-5086] fix: instance setup screen asset #7914 2025-10-07 18:47:36 +05:30
M. PalanikannanandGitHub c3e8ce8f28 [WIKI-710] [WIKI-717] fix: slash commands and mentions in comments (#7919)
* fix: fixing slash commands and mentions

* fix: tiptap types handled
2025-10-07 18:01:39 +05:30
Yaroslav HalchenkoandGitHub 2ca8620246 Add codespell support (config, workflow to detect/not fix) and make it fix some typos (#7864)
* Add github action to codespell preview on push and PRs

* Add rudimentary codespell config

* [DATALAD RUNCMD] chore: run codespell throughout fixing a few typos interactively

=== Do not change lines below ===
{
 "chain": [],
 "cmd": "codespell -w -i 3 -C 4 ./apps/web/core/components/workspace/delete-workspace-form.tsx ./deployments/cli/community/README.md",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [],
 "outputs": [],
 "pwd": "."
}
^^^ Do not change lines above ^^^

* Adjust coespell regex to ignore all camelCased words

* [DATALAD RUNCMD] chore: run codespell throughout fixing a few  new typos automagically

=== Do not change lines below ===
{
 "chain": [],
 "cmd": "codespell -w",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [],
 "outputs": [],
 "pwd": "."
}
^^^ Do not change lines above ^^^
2025-10-07 17:42:49 +05:30
sriram veeraghantaandGitHub 07ff457663 [PRIME-24] fix: update live server aio build path (#7911)
* fix: udpate live server aio build path

* chore: build path
2025-10-07 17:21:07 +05:30
Jayash TripathyandGitHub d3024f8a7f [WEB-4731] feat: add baseui input component to propel package (#7769)
*  feat: add input component to propel package

* 🚨 fix: lint

* 🚨 fix: lint

* fix: add aria-invalid attribute to Input component for better accessibility

* chore: fix formatting in package.json and reorder imports in popover-menu stories
2025-10-07 16:48:02 +05:30
M. PalanikannanandGitHub a740a9f69d [WIKI-713] fix: extra line created on enter key #7913 2025-10-07 14:22:09 +05:30
guru_sainathandGitHub 298acb1e91 [MOB-1221] dev: updated god mode ui #7879 2025-10-07 00:24:09 +05:30
Martin DagarinandGitHub f9e21c58c0 Deployment AIO fixed live entered FATAL state (#7893) 2025-10-06 23:42:21 +05:30
Dheeraj Kumar KetireddyandGitHub 5c842d592e Chore: Filter backend optimizations (#7900)
* refactor: enhance ComplexFilterBackend and BaseFilterSet for Q object filtering

- Introduced BaseFilterSet to support Q object construction for complex filtering.
- Updated ComplexFilterBackend to utilize Q objects for building querysets.
- Improved error handling and validation in filter methods.
- Refactored filter evaluation logic to streamline query construction.

* fix: improve filter processing in BaseFilterSet to handle empty cleaned_data and optimize filter evaluation

- Added handling for cases where cleaned_data is None or empty, returning an empty Q object.
- Optimized filter evaluation by only processing filters that are provided in the request data.

* update ComplexFilterBackend to pass queryset in filter evaluation
2025-10-06 23:36:16 +05:30
0589ac56d5 [WEB-5048] chore: implements esm exports for all packages (#7816)
* fix: esm module imports for live and editor

* fix: convert all pacakges to export esm and cjs build

* fix: auto export

* fix: translation files formatting

* fix: convert eslint files to cjs files for supporting current config

* fix: code uuid package upgrade

---------

Co-authored-by: Aaron Reisman <aaron.reisman@plane.so>
2025-10-06 21:01:32 +05:30
AaronandGitHub cbcb026e9a [WEB-5083] chore: add stories to propel #7888 2025-10-06 20:48:52 +05:30
Vipin ChaudharyandGitHub 0cca31ef2e [WIKI-707] [WIKI-708] fix: editor floating ui modal (#7909) 2025-10-06 16:30:47 +05:30
sriram veeraghanta 4c8f51568a chore: pmp lock upgrade 2025-10-06 01:23:09 +05:30
sriram veeraghanta 8d14a5a004 chore(deps): added vite to catalog 2025-10-06 00:56:52 +05:30
sriram veeraghanta 97381c5145 chore(deps): pnpm version upgrade 2025-10-06 00:38:32 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4709a0fca4 chore(deps): bump django (#7892)
Bumps the pip group with 1 update in the /apps/api/requirements directory: [django](https://github.com/django/django).


Updates `django` from 4.2.24 to 4.2.25
- [Commits](https://github.com/django/django/compare/4.2.24...4.2.25)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 4.2.25
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-05 19:25:09 +05:30
Prateek ShouryaandGitHub b695f161ef [WEB-5074] fix: remove the filter conditions check from the defaultOpen property to prevent unwanted popups. (#7903) 2025-10-03 19:56:31 +05:30
M. PalanikannanandGitHub 431af01483 [WIKI-704] fix: disable exiting on unhandled rejection/exception #7902 2025-10-03 16:10:21 +05:30
Aaryan KhandelwalandGitHub 269fc0d9e0 [WIKI-705] regression: mentions dropdown selection across editors #7901 2025-10-03 15:53:35 +05:30
Anmol Singh BhatiaandGitHub 3c389e2e50 [WEB-5072] chore: propel combobox improvement #7899 2025-10-03 15:01:21 +05:30
M. PalanikannanandGitHub f6677f252f [WIKI-704] fix: hocuspocus error handling (#7898) 2025-10-03 14:01:38 +05:30
Vipin ChaudharyandGitHub 794271ac17 fix-config_types (#7886) 2025-10-01 16:24:58 +05:30
Aaryan KhandelwalandGitHub 931a48d7d7 [WIKI-569] regression: parser kit extensions mismatch #7885 2025-10-01 15:27:06 +05:30
Vipin ChaudharyandGitHub b37e453b32 [WIKI-699] refactor: editor config (#7850) 2025-10-01 15:00:10 +05:30
Aaryan KhandelwalandGitHub a69c6f1b9c regression: slash commands render logic #7882 2025-10-01 02:44:31 +05:30
M. PalanikannanandGitHub ee68895ad3 [WIKI-682] feat: hocuspocus v3 server migration (#7880)
* fix: hocuspocus v3

* fix: publishing messages across redis extension
2025-09-30 20:55:55 +05:30
e110ef55b8 [WIKI-569] chore: migrate to tiptap v3 (#7526)
* chore: upgrade to tiptap v3

* chore: update starter kit

* chore: tippy to floating-ui migration for mentions

* chore: update remaining floating menus

* chore: update setEditorValue function

* fix: potential bugs

* chore: extract out common floating ui positioning logic

* fix: storage api

* fix: bubble menu

* fix: type errors

* fix: type errors

* chore: upgrade tiptap-markdown package

* fix: mentions close callback

* chore: update bubbling sequence

* chore: update package.json

* chore: update tiptap catalogs

* fix: add error handling

* fix: file plugin types

* chore: update live package.json

* fix: broken lock file

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-09-30 19:56:12 +05:30
sriram veeraghantaandGitHub 5951372555 [WIKI-679] refactor: live server (#7720) 2025-09-30 19:28:15 +05:30
Prateek ShouryaandGitHub 7ce21a6488 [WEB-4969] feat: add toggle button for work item filters row visibility (#7865)
* [WEB-4969] feat: add toggle button for work item filters row visibility

* fix: improve error handling in filter update and refine visibility condition check

* chore: correct visibility toggle parameter in filter store
2025-09-30 18:19:43 +05:30
sriram veeraghantaandGitHub 992457efd2 [WEB-5050] feat: updated work item urls with issues. (#7871)
* feat: updated work item urls with issues.

* chore: updated urls names

* chore: rename file

* chore: code refactor for url ordering

* fix: renamed issue-atachments to attachments
2025-09-30 17:47:05 +05:30
Vamsi KrishnaandGitHub c45151d5e6 [WEB-4882]feat: suspended users (#7844) 2025-09-30 15:31:56 +05:30
Anmol Singh BhatiaandGitHub 726529044e [WEB-4934] dev: propel button implementation (#7859)
* dev: button component code refactor

* dev: propel button component implementation
2025-09-30 15:31:00 +05:30
Vipin ChaudharyandGitHub 0ad439fa63 [WIKI-696] fix: peek view close on click block menu options (#7861)
* fix: block close

* chore : add toggle option in block menu

* fix: separate methods
2025-09-30 15:29:35 +05:30
Aaryan KhandelwalandGitHub 843faf85b7 [WIKI-668] fix: comments mention #7874 2025-09-30 15:27:41 +05:30
sriram veeraghantaandGitHub ded36785e7 feat: optimize project list external endpoint (#7869) 2025-09-29 20:40:33 +05:30
sriram veeraghantaandGitHub 63096ceb8c release: v1.0.0 #7711 2025-09-10 14:53:52 +05:30
sriram veeraghantaandGitHub 9b58b9259e fix: adding proxy command in compose file #7519 2025-07-30 21:03:03 +05:30
sriram veeraghantaandGitHub 1e235600b7 release: v0.28.0 #7488 2025-07-30 16:51:00 +05:30
sriram veeraghantaandGitHub 04b0d7ee9f release: v0.27.1 #7328 2025-07-04 13:31:00 +05:30
sriram veeraghantaandGitHub 5a3f709e72 release: v0.27.0 #7296 2025-07-01 17:40:56 +05:30
sriram veeraghantaandGitHub 5ef33ab798 release: v0.26.1 #7181 2025-06-06 17:55:03 +05:30
sriram veeraghantaandGitHub 27806fbe20 release: v0.26.1 #7179 2025-06-06 17:27:49 +05:30
2111 changed files with 96554 additions and 62573 deletions
+7
View File
@@ -0,0 +1,7 @@
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git*,*.svg,i18n,*-lock.yaml,*.css,.codespellrc,migrations,*.js,*.map,*.mjs
check-hidden = true
# ignore all CamelCase and camelCase
ignore-regex = \b[A-Za-z][a-z]+[A-Z][a-zA-Z]+\b
ignore-words-list = tread
+6
View File
@@ -61,3 +61,9 @@ temp/
# Misc
*.pem
*.key
# React Router - https://github.com/remix-run/react-router-templates/blob/dc79b1a065f59f3bfd840d4ef75cc27689b611e6/default/.dockerignore
.react-router/
build/
node_modules/
README.md
+25
View File
@@ -0,0 +1,25 @@
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
push:
branches: [preview]
pull_request:
branches: [preview]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@v1
- name: Codespell
uses: codespell-project/actions-codespell@v2
+7 -1
View File
@@ -16,9 +16,10 @@ node_modules
/out/
# Production
/build
dist/
out/
build/
.react-router/
# Misc
.DS_Store
@@ -99,3 +100,8 @@ dev-editor
*.rdb.gz
storybook-static
CLAUDE.md
AGENTS.md
temp/
+2
View File
@@ -0,0 +1,2 @@
[tools]
node = "22.18.0"
+14
View File
@@ -1,4 +1,18 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
rules: {
"no-duplicate-imports": "off",
"import/no-duplicates": ["error", { "prefer-inline": false }],
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{
prefer: "type-imports",
fixStyle: "separate-type-imports",
disallowTypeAnnotations: false,
},
],
},
};
+6 -4
View File
@@ -1,11 +1,13 @@
"use client";
import { FC } from "react";
import type { FC } from "react";
import { useForm } from "react-hook-form";
import { Lightbulb } from "lucide-react";
import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@plane/types";
// components
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
// hooks
import { useInstance } from "@/hooks/store";
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Artificial Intelligence Settings - God Mode",
@@ -0,0 +1,210 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
import { useForm } from "react-hook-form";
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGiteaAuthenticationConfigurationKeys } from "@plane/types";
import { Button, getButtonStyling } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
// hooks
import { useInstance } from "@/hooks/store";
type Props = {
config: IFormattedInstanceConfiguration;
};
type GiteaConfigFormValues = Record<TInstanceGiteaAuthenticationConfigurationKeys, string>;
export const InstanceGiteaConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
// store hooks
const { updateInstanceConfigurations } = useInstance();
// form data
const {
handleSubmit,
control,
reset,
formState: { errors, isDirty, isSubmitting },
} = useForm<GiteaConfigFormValues>({
defaultValues: {
GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com",
GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"],
GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"],
},
});
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const GITEA_FORM_FIELDS: TControllerInputFormField[] = [
{
key: "GITEA_HOST",
type: "text",
label: "Gitea Host",
description: (
<>Use the URL of your Gitea instance. For the official Gitea instance, use &quot;https://gitea.com&quot;.</>
),
placeholder: "https://gitea.com",
error: Boolean(errors.GITEA_HOST),
required: true,
},
{
key: "GITEA_CLIENT_ID",
type: "text",
label: "Client ID",
description: (
<>
You will get this from your{" "}
<a
tabIndex={-1}
href="https://gitea.com/user/settings/applications"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Gitea OAuth application settings.
</a>
</>
),
placeholder: "70a44354520df8bd9bcd",
error: Boolean(errors.GITEA_CLIENT_ID),
required: true,
},
{
key: "GITEA_CLIENT_SECRET",
type: "password",
label: "Client secret",
description: (
<>
Your client secret is also found in your{" "}
<a
tabIndex={-1}
href="https://gitea.com/user/settings/applications"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Gitea OAuth application settings.
</a>
</>
),
placeholder: "9b0050f94ec1b744e32ce79ea4ffacd40d4119cb",
error: Boolean(errors.GITEA_CLIENT_SECRET),
required: true,
},
];
const GITEA_SERVICE_FIELD: TCopyField[] = [
{
key: "Callback_URI",
label: "Callback URI",
url: `${originURL}/auth/gitea/callback/`,
description: (
<>
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
field{" "}
<a
tabIndex={-1}
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
</a>
</>
),
},
];
const onSubmit = async (formData: GiteaConfigFormValues) => {
const payload: Partial<GiteaConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then((response = []) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Done!",
message: "Your Gitea authentication is configured. You should test it now.",
});
reset({
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
});
})
.catch((err) => console.error(err));
};
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (isDirty) {
e.preventDefault();
setIsDiscardChangesModalOpen(true);
}
};
return (
<>
<ConfirmDiscardModal
isOpen={isDiscardChangesModalOpen}
onDiscardHref="/authentication"
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
<div className="pt-2.5 text-xl font-medium">Gitea-provided details for Plane</div>
{GITEA_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
control={control}
type={field.type}
name={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
error={field.error}
required={field.required}
/>
))}
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Plane-provided details for Gitea</div>
{GITEA_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
</div>
</div>
</div>
</>
);
};
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Gitea Authentication - God Mode",
};
export default function GiteaAuthenticationLayout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -0,0 +1,104 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
// plane internal packages
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import giteaLogo from "@/public/logos/gitea-logo.svg";
//local components
import { InstanceGiteaConfigForm } from "./form";
const InstanceGiteaAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// theme
const { resolvedTheme } = useTheme();
// config
const enableGiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? "";
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
const updateConfig = async (key: "IS_GITEA_ENABLED", value: string) => {
setIsSubmitting(true);
const payload = {
[key]: value,
};
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
},
});
await updateConfigPromise
.then(() => {
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setIsSubmitting(false);
});
};
const isGiteaEnabled = enableGiteaConfig === "1";
return (
<>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="Gitea"
description="Allow members to login or sign up to plane with their Gitea accounts."
icon={<Image src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
config={
<ToggleSwitch
value={isGiteaEnabled}
onChange={() => {
updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceGiteaConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</div>
</div>
</>
);
});
export default InstanceGiteaAuthenticationPage;
@@ -1,19 +1,25 @@
"use client";
import { FC, useState } from "react";
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { Monitor } from "lucide-react";
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
import { CopyField, TCopyField } from "@/components/common/copy-field";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
// hooks
import { useInstance } from "@/hooks/store";
@@ -101,7 +107,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
},
];
const GITHUB_SERVICE_FIELD: TCopyField[] = [
const GITHUB_COMMON_SERVICE_DETAILS: TCopyField[] = [
{
key: "Origin_URL",
label: "Origin URL",
@@ -121,6 +127,9 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
</>
),
},
];
const GITHUB_SERVICE_DETAILS: TCopyField[] = [
{
key: "Callback_URI",
label: "Callback URI",
@@ -208,12 +217,29 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Plane-provided details for GitHub</div>
{GITHUB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
<div className="pt-2 text-xl font-medium">Plane-provided details for GitHub</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
{/* web service details */}
<div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
<Monitor className="w-3 h-3" />
Web
</div>
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
{GITHUB_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
</div>
</div>
</div>
</div>
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "GitHub Authentication - God Mode",
@@ -6,7 +6,8 @@ import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
// plane internal packages
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
import { resolveGeneralTheme } from "@plane/utils";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
@@ -43,7 +44,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitHub authentication is now ${value ? "active" : "disabled"}.`,
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -1,17 +1,21 @@
import { FC, useState } from "react";
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
import { useForm } from "react-hook-form";
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
import { CopyField, TCopyField } from "@/components/common/copy-field";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
// hooks
import { useInstance } from "@/hooks/store";
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "GitLab Authentication - God Mode",
@@ -4,7 +4,8 @@ import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
// hooks
@@ -37,7 +38,7 @@ const InstanceGitlabAuthenticationPage = observer(() => {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitLab authentication is now ${value ? "active" : "disabled"}.`,
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -1,18 +1,23 @@
"use client";
import { FC, useState } from "react";
import type { FC } from "react";
import { useState } from "react";
import { isEmpty } from "lodash-es";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { Monitor } from "lucide-react";
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { CodeBlock } from "@/components/common/code-block";
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
import { CopyField, TCopyField } from "@/components/common/copy-field";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
import type { TCopyField } from "@/components/common/copy-field";
import { CopyField } from "@/components/common/copy-field";
// hooks
import { useInstance } from "@/hooks/store";
@@ -90,7 +95,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
},
];
const GOOGLE_SERVICE_DETAILS: TCopyField[] = [
const GOOGLE_COMMON_SERVICE_DETAILS: TCopyField[] = [
{
key: "Origin_URL",
label: "Origin URL",
@@ -110,6 +115,9 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
</p>
),
},
];
const GOOGLE_SERVICE_DETAILS: TCopyField[] = [
{
key: "Callback_URI",
label: "Callback URI",
@@ -195,12 +203,29 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Plane-provided details for Google</div>
{GOOGLE_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
<div className="pt-2 text-xl font-medium">Plane-provided details for Google</div>
<div className="flex flex-col gap-y-4">
{/* common service details */}
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
{/* web service details */}
<div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
<Monitor className="w-3 h-3" />
Web
</div>
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
{GOOGLE_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
</div>
</div>
</div>
</div>
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Google Authentication - God Mode",
@@ -4,7 +4,8 @@ import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
// hooks
@@ -37,7 +38,7 @@ const InstanceGoogleAuthenticationPage = observer(() => {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Google authentication is now ${value ? "active" : "disabled"}.`,
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Authentication Settings - Plane Web",
@@ -4,8 +4,9 @@ import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane internal packages
import { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
import { setPromiseToast } from "@plane/propel/toast";
import type { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";
@@ -1,13 +1,17 @@
"use client";
import React, { FC, useMemo, useState } from "react";
import type { FC } from "react";
import React, { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
// types
import { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types";
// ui
import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
import { CustomSelect } from "@plane/ui";
// components
import { ControllerInput, TControllerInputFormField } from "@/components/common/controller-input";
import type { TControllerInputFormField } from "@/components/common/controller-input";
import { ControllerInput } from "@/components/common/controller-input";
// hooks
import { useInstance } from "@/hooks/store";
// local components
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
interface EmailLayoutProps {
children: ReactNode;
@@ -3,7 +3,8 @@
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader, setToast, TOAST_TYPE, ToggleSwitch } from "@plane/ui";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -1,9 +1,11 @@
import React, { FC, useEffect, useState } from "react";
import type { FC } from "react";
import React, { useEffect, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports
import { Button } from "@plane/propel/button";
import { InstanceService } from "@plane/services";
// ui
import { Button, Input } from "@plane/ui";
import { Input } from "@plane/ui";
type Props = {
isOpen: boolean;
@@ -1,12 +1,14 @@
"use client";
import { FC } from "react";
import type { FC } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { Telescope } from "lucide-react";
// types
import { IInstance, IInstanceAdmin } from "@plane/types";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IInstance, IInstanceAdmin } from "@plane/types";
// ui
import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
import { Input, ToggleSwitch } from "@plane/ui";
// components
import { ControllerInput } from "@/components/common/controller-input";
import { useInstance } from "@/hooks/store";
@@ -1,10 +1,11 @@
"use client";
import { FC, useState } from "react";
import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
import { MessageSquare } from "lucide-react";
import { IFormattedInstanceConfiguration } from "@plane/types";
import type { IFormattedInstanceConfiguration } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
// hooks
import { useInstance } from "@/hooks/store";
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "General Settings - God Mode",
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { FC } from "react";
import type { FC } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Menu, Settings } from "lucide-react";
@@ -1,8 +1,9 @@
"use client";
import { FC } from "react";
import type { FC } from "react";
import { useForm } from "react-hook-form";
import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from "@plane/types";
// components
import { ControllerInput } from "@/components/common/controller-input";
// hooks
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
interface ImageLayoutProps {
children: ReactNode;
+2 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { FC, ReactNode, useEffect } from "react";
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/navigation";
// components
@@ -1,13 +1,14 @@
"use client";
import { FC, useState, useRef } from "react";
import type { FC } from "react";
import { useState, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { ExternalLink, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { DiscordIcon, GithubIcon } from "@plane/propel/icons";
import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
@@ -20,7 +21,7 @@ const helpOptions = [
{
name: "Documentation",
href: "https://docs.plane.so/",
Icon: FileText,
Icon: PageIcon,
},
{
name: "Join our Discord",
@@ -110,7 +111,7 @@ export const AdminSidebarHelpSection: FC = observer(() => {
<Link href={href} key={name} target="_blank">
<div className="flex items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80">
<div className="grid flex-shrink-0 place-items-center">
<Icon className="h-3.5 w-3.5 text-custom-text-200" size={14} />
<Icon className="h-3.5 w-3.5 text-custom-text-200" width={14} height={14} />
</div>
<span className="text-xs">{name}</span>
</div>
+2 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { FC, useEffect, useRef } from "react";
import type { FC } from "react";
import { useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
@@ -4,10 +4,12 @@ import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
// plane imports
import { WEB_BASE_URL, ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { InstanceWorkspaceService } from "@plane/services";
import { IWorkspace } from "@plane/types";
import type { IWorkspace } from "@plane/types";
// components
import { Button, CustomSelect, getButtonStyling, Input, setToast, TOAST_TYPE } from "@plane/ui";
import { CustomSelect, Input } from "@plane/ui";
// hooks
import { useWorkspace } from "@/hooks/store";
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Workspace Management - God Mode",
@@ -6,8 +6,11 @@ import Link from "next/link";
import useSWR from "swr";
import { Loader as LoaderIcon } from "lucide-react";
// types
import { TInstanceConfigurationKeys } from "@plane/types";
import { Button, getButtonStyling, Loader, setPromiseToast, ToggleSwitch } from "@plane/ui";
import { Button, getButtonStyling } from "@plane/propel/button";
import { setPromiseToast } from "@plane/propel/toast";
import type { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { WorkspaceListItem } from "@/components/workspace/list-item";
+2 -2
View File
@@ -1,7 +1,7 @@
import { FC } from "react";
import type { FC } from "react";
import { Info, X } from "lucide-react";
// plane constants
import { TAdminAuthErrorInfo } from "@plane/constants";
import type { TAdminAuthErrorInfo } from "@plane/constants";
type TAuthBanner = {
bannerData: TAdminAuthErrorInfo | undefined;
+4 -3
View File
@@ -1,10 +1,11 @@
import { ReactNode } from "react";
import type { ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
import { KeyRound, Mails } from "lucide-react";
// plane packages
import { SUPPORT_EMAIL, EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
import { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
import type { TAdminAuthErrorInfo } from "@plane/constants";
import { SUPPORT_EMAIL, EAdminAuthErrorCodes } from "@plane/constants";
import type { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
import { resolveGeneralTheme } from "@plane/utils";
// components
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
+6 -3
View File
@@ -1,12 +1,15 @@
"use client";
import { FC, useEffect, useMemo, useState } from "react";
import type { FC } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { Eye, EyeOff } from "lucide-react";
// plane internal packages
import { API_BASE_URL, EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
import type { EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
import { API_BASE_URL } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { AuthService } from "@plane/services";
import { Button, Input, Spinner } from "@plane/ui";
import { Input, Spinner } from "@plane/ui";
// components
import { Banner } from "@/components/common/banner";
// local components
+1 -1
View File
@@ -1,4 +1,4 @@
import { FC, ReactNode } from "react";
import type { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// hooks
+2 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { ReactNode, createContext } from "react";
import type { ReactNode } from "react";
import { createContext } from "react";
// plane admin store
import { RootStore } from "@/plane-admin/store/root.store";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import { useTheme } from "next-themes";
import { Toast } from "@plane/ui";
import { Toast } from "@plane/propel/toast";
import { resolveGeneralTheme } from "@plane/utils";
export const ToastWithTheme = () => {
+2 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { FC, ReactNode, useEffect } from "react";
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// hooks
+2 -2
View File
@@ -1,5 +1,5 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import type { ReactNode } from "react";
import type { Metadata } from "next";
// plane imports
import { ADMIN_BASE_PATH } from "@plane/constants";
// styles
@@ -3,7 +3,7 @@ import Image from "next/image";
import { useTheme } from "next-themes";
import { KeyRound, Mails } from "lucide-react";
// types
import {
import type {
TGetBaseAuthenticationModeProps,
TInstanceAuthenticationMethodKeys,
TInstanceAuthenticationModes,
@@ -12,6 +12,7 @@ import { resolveGeneralTheme } from "@plane/utils";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
import { GiteaConfiguration } from "@/components/authentication/gitea-config";
import { GithubConfiguration } from "@/components/authentication/github-config";
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
import { GoogleConfiguration } from "@/components/authentication/google-config";
@@ -19,6 +20,7 @@ import { PasswordLoginConfiguration } from "@/components/authentication/password
// plane admin components
import { UpgradeButton } from "@/plane-admin/components/common";
// assets
import giteaLogo from "@/public/logos/gitea-logo.svg";
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
@@ -80,6 +82,13 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "gitea",
name: "Gitea",
description: "Allow members to log in or sign up to plane with their Gitea accounts.",
icon: <Image src={giteaLogo} height={20} width={20} alt="Gitea Logo" />,
config: <GiteaConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "oidc",
name: "OIDC",
@@ -4,7 +4,7 @@ import React from "react";
// icons
import { SquareArrowOutUpRight } from "lucide-react";
// plane internal packages
import { getButtonStyling } from "@plane/ui";
import { getButtonStyling } from "@plane/propel/button";
import { cn } from "@plane/utils";
export const UpgradeButton: React.FC = () => (
@@ -1,6 +1,6 @@
"use client";
import { FC } from "react";
import type { FC } from "react";
// helpers
import { cn } from "@plane/utils";
@@ -3,7 +3,7 @@
import React from "react";
import { observer } from "mobx-react";
// hooks
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { useInstance } from "@/hooks/store";
// ui
@@ -0,0 +1,58 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// plane internal packages
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";
type Props = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const GiteaConfiguration: React.FC<Props> = observer((props) => {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
// derived values
const GiteaConfig = formattedConfig?.IS_GITEA_ENABLED ?? "";
const GiteaConfigured =
!!formattedConfig?.GITEA_HOST && !!formattedConfig?.GITEA_CLIENT_ID && !!formattedConfig?.GITEA_CLIENT_SECRET;
return (
<>
{GiteaConfigured ? (
<div className="flex items-center gap-4">
<Link href="/authentication/gitea" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
Edit
</Link>
<ToggleSwitch
value={Boolean(parseInt(GiteaConfig))}
onChange={() => {
Boolean(parseInt(GiteaConfig)) === true
? updateConfig("IS_GITEA_ENABLED", "0")
: updateConfig("IS_GITEA_ENABLED", "1");
}}
size="sm"
disabled={disabled}
/>
</div>
) : (
<Link
href="/authentication/gitea"
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
>
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
Configure
</Link>
)}
</>
);
});
@@ -6,8 +6,9 @@ import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// plane internal packages
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
import { getButtonStyling } from "@plane/propel/button";
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";
@@ -6,8 +6,9 @@ import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// plane internal packages
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
import { getButtonStyling } from "@plane/propel/button";
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";
@@ -6,8 +6,9 @@ import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// plane internal packages
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
import { getButtonStyling } from "@plane/propel/button";
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";
@@ -3,7 +3,7 @@
import React from "react";
import { observer } from "mobx-react";
// hooks
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { useInstance } from "@/hooks/store";
// ui
+1 -1
View File
@@ -1,4 +1,4 @@
import { FC } from "react";
import type { FC } from "react";
import { AlertCircle, CheckCircle2 } from "lucide-react";
type TBanner = {
@@ -5,7 +5,7 @@ import Link from "next/link";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui
import { Button, getButtonStyling } from "@plane/ui";
import { Button, getButtonStyling } from "@plane/propel/button";
type Props = {
isOpen: boolean;
@@ -1,7 +1,8 @@
"use client";
import React, { useState } from "react";
import { Controller, Control } from "react-hook-form";
import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
// icons
import { Eye, EyeOff } from "lucide-react";
// plane internal packages
@@ -3,8 +3,8 @@
import React from "react";
// ui
import { Copy } from "lucide-react";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// icons
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
type Props = {
label: string;
@@ -2,7 +2,7 @@
import React from "react";
import Image from "next/image";
import { Button } from "@plane/ui";
import { Button } from "@plane/propel/button";
type Props = {
title: string;
@@ -1,9 +1,9 @@
"use client";
import { FC } from "react";
import type { FC } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Button } from "@plane/ui";
import { Button } from "@plane/propel/button";
// assets
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
@@ -1,9 +1,9 @@
"use client";
import { FC } from "react";
import type { FC } from "react";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@plane/ui";
import { Button } from "@plane/propel/button";
// assets
import PlaneTakeOffImage from "@/public/images/plane-takeoff.png";
@@ -1,13 +1,15 @@
"use client";
import { FC, useEffect, useMemo, useState } from "react";
import type { FC } from "react";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
// icons
import { Eye, EyeOff } from "lucide-react";
// plane internal packages
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { AuthService } from "@plane/services";
import { Button, Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
import { getPasswordStrength } from "@plane/utils";
// components
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
@@ -6,7 +6,7 @@ import Image from "next/image";
import Link from "next/link";
import { useTheme as nextUseTheme } from "next-themes";
// ui
import { Button, getButtonStyling } from "@plane/ui";
import { Button, getButtonStyling } from "@plane/propel/button";
import { resolveGeneralTheme } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
+1 -1
View File
@@ -1,7 +1,7 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/app/(all)/store.provider";
import { IInstanceStore } from "@/store/instance.store";
import type { IInstanceStore } from "@/store/instance.store";
export const useInstance = (): IInstanceStore => {
const context = useContext(StoreContext);
+1 -1
View File
@@ -1,7 +1,7 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/app/(all)/store.provider";
import { IThemeStore } from "@/store/theme.store";
import type { IThemeStore } from "@/store/theme.store";
export const useTheme = (): IThemeStore => {
const context = useContext(StoreContext);
+1 -1
View File
@@ -1,7 +1,7 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/app/(all)/store.provider";
import { IUserStore } from "@/store/user.store";
import type { IUserStore } from "@/store/user.store";
export const useUser = (): IUserStore => {
const context = useContext(StoreContext);
@@ -1,7 +1,7 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/app/(all)/store.provider";
import { IWorkspaceStore } from "@/store/workspace.store";
import type { IWorkspaceStore } from "@/store/workspace.store";
export const useWorkspace = (): IWorkspaceStore => {
const context = useContext(StoreContext);
+4 -3
View File
@@ -1,9 +1,10 @@
import { set } from "lodash-es";
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// plane internal packages
import { EInstanceStatus, TInstanceStatus } from "@plane/constants";
import type { TInstanceStatus } from "@plane/constants";
import { EInstanceStatus } from "@plane/constants";
import { InstanceService } from "@plane/services";
import {
import type {
IInstance,
IInstanceAdmin,
IInstanceConfiguration,
@@ -12,7 +13,7 @@ import {
IInstanceConfig,
} from "@plane/types";
// root store
import { CoreRootStore } from "@/store/root.store";
import type { CoreRootStore } from "@/store/root.store";
export interface IInstanceStore {
// issues
+8 -4
View File
@@ -1,9 +1,13 @@
import { enableStaticRendering } from "mobx-react";
// stores
import { IInstanceStore, InstanceStore } from "./instance.store";
import { IThemeStore, ThemeStore } from "./theme.store";
import { IUserStore, UserStore } from "./user.store";
import { IWorkspaceStore, WorkspaceStore } from "./workspace.store";
import type { IInstanceStore } from "./instance.store";
import { InstanceStore } from "./instance.store";
import type { IThemeStore } from "./theme.store";
import { ThemeStore } from "./theme.store";
import type { IUserStore } from "./user.store";
import { UserStore } from "./user.store";
import type { IWorkspaceStore } from "./workspace.store";
import { WorkspaceStore } from "./workspace.store";
enableStaticRendering(typeof window === "undefined");
+1 -1
View File
@@ -1,6 +1,6 @@
import { action, observable, makeObservable } from "mobx";
// root store
import { CoreRootStore } from "@/store/root.store";
import type { CoreRootStore } from "@/store/root.store";
type TTheme = "dark" | "light";
export interface IThemeStore {
+4 -3
View File
@@ -1,10 +1,11 @@
import { action, observable, runInAction, makeObservable } from "mobx";
// plane internal packages
import { EUserStatus, TUserStatus } from "@plane/constants";
import type { TUserStatus } from "@plane/constants";
import { EUserStatus } from "@plane/constants";
import { AuthService, UserService } from "@plane/services";
import { IUser } from "@plane/types";
import type { IUser } from "@plane/types";
// root store
import { CoreRootStore } from "@/store/root.store";
import type { CoreRootStore } from "@/store/root.store";
export interface IUserStore {
// observables
+2 -2
View File
@@ -2,9 +2,9 @@ import { set } from "lodash-es";
import { action, observable, runInAction, makeObservable, computed } from "mobx";
// plane imports
import { InstanceWorkspaceService } from "@plane/services";
import { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
import type { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
// root store
import { CoreRootStore } from "@/store/root.store";
import type { CoreRootStore } from "@/store/root.store";
export interface IWorkspaceStore {
// observables
+2 -3
View File
@@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "1.0.0",
"version": "1.1.0",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@@ -46,10 +46,9 @@
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@types/lodash-es": "catalog:",
"@types/node": "18.16.1",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/uuid": "^9.0.8",
"typescript": "catalog:"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+21 -21
View File
@@ -111,6 +111,27 @@
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
/* toast theme */
--color-toast-success-text: 178, 221, 181;
--color-toast-error-text: 206, 44, 49;
--color-toast-warning-text: 255, 186, 24;
--color-toast-info-text: 141, 164, 239;
--color-toast-loading-text: 255, 255, 255;
--color-toast-secondary-text: 185, 187, 198;
--color-toast-tertiary-text: 139, 141, 152;
--color-toast-success-background: 46, 46, 46;
--color-toast-error-background: 46, 46, 46;
--color-toast-warning-background: 46, 46, 46;
--color-toast-info-background: 46, 46, 46;
--color-toast-loading-background: 46, 46, 46;
--color-toast-success-border: 42, 126, 59;
--color-toast-error-border: 100, 23, 35;
--color-toast-warning-border: 79, 52, 34;
--color-toast-info-border: 58, 91, 199;
--color-toast-loading-border: 96, 100, 108;
}
[data-theme="light"],
@@ -221,27 +242,6 @@
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
/* toast theme */
--color-toast-success-text: 178, 221, 181;
--color-toast-error-text: 206, 44, 49;
--color-toast-warning-text: 255, 186, 24;
--color-toast-info-text: 141, 164, 239;
--color-toast-loading-text: 255, 255, 255;
--color-toast-secondary-text: 185, 187, 198;
--color-toast-tertiary-text: 139, 141, 152;
--color-toast-success-background: 46, 46, 46;
--color-toast-error-background: 46, 46, 46;
--color-toast-warning-background: 46, 46, 46;
--color-toast-info-background: 46, 46, 46;
--color-toast-loading-background: 46, 46, 46;
--color-toast-success-border: 42, 126, 59;
--color-toast-error-border: 100, 23, 35;
--color-toast-warning-border: 79, 52, 34;
--color-toast-info-border: 58, 91, 199;
--color-toast-loading-border: 96, 100, 108;
}
[data-theme="dark-contrast"] {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "1.0.0",
"version": "1.1.0",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
+12 -1
View File
@@ -4,7 +4,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.db.models import Cycle, CycleIssue
from plane.db.models import Cycle, CycleIssue, User
from plane.utils.timezone_converter import convert_to_utc
@@ -16,6 +16,13 @@ class CycleCreateSerializer(BaseSerializer):
and UTC normalization for time-bound iteration planning and sprint management.
"""
owned_by = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
required=False,
allow_null=True,
help_text="User who owns the cycle. If not provided, defaults to the current user.",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
project = self.context.get("project")
@@ -72,6 +79,10 @@ class CycleCreateSerializer(BaseSerializer):
date=str(data.get("end_date", None).date()),
project_id=project_id,
)
if not data.get("owned_by"):
data["owned_by"] = self.context["request"].user
return data
+1 -1
View File
@@ -1,4 +1,4 @@
# Module improts
# Module imports
from .base import BaseSerializer
from .issue import IssueExpandSerializer
from plane.db.models import IntakeIssue, Issue
+54 -1
View File
@@ -1,4 +1,5 @@
# Third party imports
import random
from rest_framework import serializers
# Module imports
@@ -24,6 +25,47 @@ class ProjectCreateSerializer(BaseSerializer):
and workspace association for new project initialization.
"""
PROJECT_ICON_DEFAULT_COLORS = [
"#95999f",
"#6d7b8a",
"#5e6ad2",
"#02b5ed",
"#02b55c",
"#f2be02",
"#e57a00",
"#f38e82",
]
PROJECT_ICON_DEFAULT_ICONS = [
"home",
"apps",
"settings",
"star",
"favorite",
"done",
"check_circle",
"add_task",
"create_new_folder",
"dataset",
"terminal",
"key",
"rocket",
"public",
"quiz",
"mood",
"gavel",
"eco",
"diamond",
"forest",
"bolt",
"sync",
"cached",
"library_add",
"view_timeline",
"view_kanban",
"empty_dashboard",
"cycle",
]
class Meta:
model = Project
fields = [
@@ -44,10 +86,10 @@ class ProjectCreateSerializer(BaseSerializer):
"archive_in",
"close_in",
"timezone",
"logo_props",
"external_source",
"external_id",
"is_issue_type_enabled",
"is_time_tracking_enabled",
]
read_only_fields = [
@@ -57,6 +99,7 @@ class ProjectCreateSerializer(BaseSerializer):
"updated_at",
"created_by",
"updated_by",
"logo_props",
]
def validate(self, data):
@@ -86,6 +129,16 @@ class ProjectCreateSerializer(BaseSerializer):
if ProjectIdentifier.objects.filter(name=identifier, workspace_id=self.context["workspace_id"]).exists():
raise serializers.ValidationError(detail="Project Identifier is taken")
if validated_data.get("logo_props", None) is None:
# Generate a random icon and color for the project icon
validated_data["logo_props"] = {
"in_use": "icon",
"icon": {
"name": random.choice(self.PROJECT_ICON_DEFAULT_ICONS),
"color": random.choice(self.PROJECT_ICON_DEFAULT_COLORS),
},
}
project = Project.objects.create(**validated_data, workspace_id=self.context["workspace_id"])
return project
+13 -11
View File
@@ -1,21 +1,23 @@
from .asset import urlpatterns as asset_patterns
from .cycle import urlpatterns as cycle_patterns
from .intake import urlpatterns as intake_patterns
from .label import urlpatterns as label_patterns
from .member import urlpatterns as member_patterns
from .module import urlpatterns as module_patterns
from .project import urlpatterns as project_patterns
from .state import urlpatterns as state_patterns
from .issue import urlpatterns as issue_patterns
from .cycle import urlpatterns as cycle_patterns
from .module import urlpatterns as module_patterns
from .intake import urlpatterns as intake_patterns
from .member import urlpatterns as member_patterns
from .asset import urlpatterns as asset_patterns
from .user import urlpatterns as user_patterns
from .work_item import urlpatterns as work_item_patterns
urlpatterns = [
*asset_patterns,
*cycle_patterns,
*intake_patterns,
*label_patterns,
*member_patterns,
*module_patterns,
*project_patterns,
*state_patterns,
*issue_patterns,
*cycle_patterns,
*module_patterns,
*intake_patterns,
*member_patterns,
*user_patterns,
*work_item_patterns,
]
+17
View File
@@ -0,0 +1,17 @@
from django.urls import path
from plane.api.views import LabelListCreateAPIEndpoint, LabelDetailAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/",
LabelListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="label",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/<uuid:pk>/",
LabelDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="label",
),
]
@@ -3,8 +3,6 @@ from django.urls import path
from plane.api.views import (
IssueListCreateAPIEndpoint,
IssueDetailAPIEndpoint,
LabelListCreateAPIEndpoint,
LabelDetailAPIEndpoint,
IssueLinkListCreateAPIEndpoint,
IssueLinkDetailAPIEndpoint,
IssueCommentListCreateAPIEndpoint,
@@ -17,7 +15,8 @@ from plane.api.views import (
IssueSearchEndpoint,
)
urlpatterns = [
# Deprecated url patterns
old_url_patterns = [
path(
"workspaces/<str:slug>/issues/search/",
IssueSearchEndpoint.as_view(http_method_names=["get"]),
@@ -38,16 +37,6 @@ urlpatterns = [
IssueDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/",
LabelListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="label",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/<uuid:pk>/",
LabelDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="label",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/",
IssueLinkListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
@@ -89,3 +78,69 @@ urlpatterns = [
name="issue-attachment",
),
]
# New url patterns with work-items as the prefix
new_url_patterns = [
path(
"workspaces/<str:slug>/work-items/search/",
IssueSearchEndpoint.as_view(http_method_names=["get"]),
name="work-item-search",
),
path(
"workspaces/<str:slug>/work-items/<str:project_identifier>-<str:issue_identifier>/",
WorkspaceIssueAPIEndpoint.as_view(http_method_names=["get"]),
name="work-item-by-identifier",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/",
IssueListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="work-item-list",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:pk>/",
IssueDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="work-item-detail",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/links/",
IssueLinkListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="work-item-link-list",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/links/<uuid:pk>/",
IssueLinkDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="work-item-link-detail",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/comments/",
IssueCommentListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="work-item-comment-list",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="work-item-comment-detail",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/activities/",
IssueActivityListAPIEndpoint.as_view(http_method_names=["get"]),
name="work-item-activity-list",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/activities/<uuid:pk>/",
IssueActivityDetailAPIEndpoint.as_view(http_method_names=["get"]),
name="work-item-activity-detail",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/attachments/",
IssueAttachmentListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="work-item-attachment-list",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/attachments/<uuid:pk>/",
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="work-item-attachment-detail",
),
]
urlpatterns = old_url_patterns + new_url_patterns
+96 -424
View File
@@ -12,13 +12,7 @@ from django.db.models import (
OuterRef,
Q,
Sum,
FloatField,
Case,
When,
Value,
)
from django.db.models.functions import Cast, Concat
from django.db import models
# Third party imports
from rest_framework import status
@@ -47,7 +41,7 @@ from plane.db.models import (
ProjectMember,
UserFavorite,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.cycle_transfer_issues import transfer_cycle_issues
from plane.utils.host import base_host
from .base import BaseAPIView
from plane.bgtasks.webhook_task import model_activity
@@ -201,7 +195,9 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now())
queryset = queryset.filter(
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = CycleSerializer(
queryset,
many=True,
@@ -258,7 +254,9 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True))
queryset = queryset.filter(
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
)
return self.paginate(
request=request,
queryset=(queryset),
@@ -304,10 +302,17 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
Create a new development cycle with specified name, description, and date range.
Supports external ID tracking for integration purposes.
"""
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
if (
request.data.get("start_date", None) is None
and request.data.get("end_date", None) is None
) or (
request.data.get("start_date", None) is not None
and request.data.get("end_date", None) is not None
):
serializer = CycleCreateSerializer(data=request.data)
serializer = CycleCreateSerializer(
data=request.data, context={"request": request}
)
if serializer.is_valid():
if (
request.data.get("external_id")
@@ -332,7 +337,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(project_id=project_id, owned_by=request.user)
serializer.save(project_id=project_id)
# Send the model activity
model_activity.delay(
model_name="cycle",
@@ -350,7 +355,9 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
{"error": "Both start date and end date are either required or are to be null"},
{
"error": "Both start date and end date are either required or are to be null"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -498,7 +505,9 @@ class CycleDetailAPIEndpoint(BaseAPIView):
"""
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
)
if cycle.archived_at:
return Response(
@@ -511,14 +520,20 @@ class CycleDetailAPIEndpoint(BaseAPIView):
if cycle.end_date is not None and cycle.end_date < timezone.now():
if "sort_order" in request_data:
# Can only change sort order
request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
request_data = {
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else:
return Response(
{"error": "The Cycle has already been completed so it cannot be edited"},
{
"error": "The Cycle has already been completed so it cannot be edited"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleUpdateSerializer(cycle, data=request.data, partial=True)
serializer = CycleUpdateSerializer(
cycle, data=request.data, partial=True, context={"request": request}
)
if serializer.is_valid():
if (
request.data.get("external_id")
@@ -526,7 +541,9 @@ class CycleDetailAPIEndpoint(BaseAPIView):
and Cycle.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", cycle.external_source),
external_source=request.data.get(
"external_source", cycle.external_source
),
external_id=request.data.get("external_id"),
).exists()
):
@@ -583,7 +600,11 @@ class CycleDetailAPIEndpoint(BaseAPIView):
status=status.HTTP_403_FORBIDDEN,
)
cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="cycle.activity.deleted",
@@ -603,7 +624,9 @@ class CycleDetailAPIEndpoint(BaseAPIView):
# Delete the cycle
cycle.delete()
# Delete the user favorite cycle
UserFavorite.objects.filter(entity_type="cycle", entity_identifier=pk, project_id=project_id).delete()
UserFavorite.objects.filter(
entity_type="cycle", entity_identifier=pk, project_id=project_id
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -741,7 +764,9 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda cycles: CycleSerializer(cycles, many=True, fields=self.fields, expand=self.expand).data,
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@cycle_docs(
@@ -760,7 +785,9 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
Move a completed cycle to archived status for historical tracking.
Only cycles that have ended can be archived.
"""
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
cycle = Cycle.objects.get(
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
if cycle.end_date >= timezone.now():
return Response(
{"error": "Only completed cycles can be archived"},
@@ -791,7 +818,9 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
Restore an archived cycle to active status, making it available for regular use.
The cycle will reappear in active cycle lists.
"""
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
cycle = Cycle.objects.get(
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
cycle.archived_at = None
cycle.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -854,7 +883,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
# List
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.issue_objects.filter(
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
@@ -891,7 +922,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
return self.paginate(
request=request,
queryset=(issues),
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
on_results=lambda issues: IssueSerializer(
issues, many=True, fields=self.fields, expand=self.expand
).data,
)
@cycle_docs(
@@ -921,10 +954,13 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
if not issues:
return Response(
{"error": "Work items are required", "code": "MISSING_WORK_ITEMS"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Work items are required", "code": "MISSING_WORK_ITEMS"},
status=status.HTTP_400_BAD_REQUEST,
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=cycle_id
)
if cycle.end_date is not None and cycle.end_date < timezone.now():
return Response(
@@ -936,9 +972,13 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
)
# Get all CycleWorkItems already created
cycle_issues = list(CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues))
cycle_issues = list(
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
)
existing_issues = [
str(cycle_issue.issue_id) for cycle_issue in cycle_issues if str(cycle_issue.issue_id) in issues
str(cycle_issue.issue_id)
for cycle_issue in cycle_issues
if str(cycle_issue.issue_id) in issues
]
new_issues = list(set(issues) - set(existing_issues))
@@ -989,7 +1029,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
current_instance=json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize("json", created_records),
"created_cycle_issues": serializers.serialize(
"json", created_records
),
}
),
epoch=int(timezone.now().timestamp()),
@@ -1065,7 +1107,9 @@ class CycleIssueDetailAPIEndpoint(BaseAPIView):
cycle_id=cycle_id,
issue_id=issue_id,
)
serializer = CycleIssueSerializer(cycle_issue, fields=self.fields, expand=self.expand)
serializer = CycleIssueSerializer(
cycle_issue, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK)
@cycle_docs(
@@ -1170,406 +1214,34 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{"error": "New Cycle Id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first()
old_cycle = (
Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id)
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
)
old_cycle = old_cycle.first()
estimate_type = Project.objects.filter(
old_cycle = Cycle.objects.get(
workspace__slug=slug,
pk=project_id,
estimate__isnull=False,
estimate__type="points",
).exists()
if estimate_type:
assignee_estimate_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.annotate(
avatar_url=Case(
# If `avatar_asset` exists, use it to generate the asset URL
When(
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
Value("/"),
),
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
),
default=Value(None),
output_field=models.CharField(),
)
)
.values("display_name", "assignee_id", "avatar", "avatar_url")
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
# assignee distribution serialization
assignee_estimate_distribution = [
{
"display_name": item["display_name"],
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
"avatar": item.get("avatar", None),
"avatar_url": item.get("avatar_url", None),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
}
for item in assignee_estimate_data
]
label_distribution_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
estimate_completion_chart = burndown_plot(
queryset=old_cycle,
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=cycle_id,
)
# Label distribution serialization
label_estimate_distribution = [
{
"label_name": item["label_name"],
"color": item["color"],
"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"],
}
for item in label_distribution_data
]
# Get the assignee distribution
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.annotate(
avatar_url=Case(
# If `avatar_asset` exists, use it to generate the asset URL
When(
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
Value("/"),
),
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
default=Value(None),
output_field=models.CharField(),
)
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
.annotate(
completed_issues=Count(
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
# assignee distribution serialized
assignee_distribution_data = [
{
"display_name": item["display_name"],
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
"avatar": item.get("avatar", None),
"avatar_url": item.get("avatar_url", None),
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
}
for item in assignee_distribution
]
# Get the label distribution
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
.annotate(
completed_issues=Count(
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
# Label distribution serilization
label_distribution_data = [
{
"label_name": item["label_name"],
"color": item["color"],
"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"],
}
for item in label_distribution
]
# Pass the new_cycle queryset to burndown_plot
completion_chart = burndown_plot(
queryset=old_cycle,
slug=slug,
project_id=project_id,
plot_type="issues",
cycle_id=cycle_id,
pk=cycle_id,
)
current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first()
current_cycle.progress_snapshot = {
"total_issues": old_cycle.total_issues,
"completed_issues": old_cycle.completed_issues,
"cancelled_issues": old_cycle.cancelled_issues,
"started_issues": old_cycle.started_issues,
"unstarted_issues": old_cycle.unstarted_issues,
"backlog_issues": old_cycle.backlog_issues,
"distribution": {
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
"completion_chart": completion_chart,
},
"estimate_distribution": (
{}
if not estimate_type
else {
"labels": label_estimate_distribution,
"assignees": assignee_estimate_distribution,
"completion_chart": estimate_completion_chart,
}
),
}
current_cycle.save(update_fields=["progress_snapshot"])
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
# transfer work items only when cycle is completed (passed the end data)
if old_cycle.end_date is not None and old_cycle.end_date > timezone.now():
return Response(
{"error": "The cycle where the issues are transferred is already completed"},
{"error": "The old cycle is not completed yet"},
status=status.HTTP_400_BAD_REQUEST,
)
cycle_issues = CycleIssue.objects.filter(
cycle_id=cycle_id,
# Call the utility function to handle the transfer
result = transfer_cycle_issues(
slug=slug,
project_id=project_id,
workspace__slug=slug,
issue__state__group__in=["backlog", "unstarted", "started"],
cycle_id=cycle_id,
new_cycle_id=new_cycle_id,
request=request,
user_id=self.request.user.id,
)
updated_cycles = []
update_cycle_issue_activity = []
for cycle_issue in cycle_issues:
cycle_issue.cycle_id = new_cycle_id
updated_cycles.append(cycle_issue)
update_cycle_issue_activity.append(
{
"old_cycle_id": str(cycle_id),
"new_cycle_id": str(new_cycle_id),
"issue_id": str(cycle_issue.issue_id),
}
# Handle the result
if result.get("success"):
return Response({"message": "Success"}, status=status.HTTP_200_OK)
else:
return Response(
{"error": result.get("error")},
status=status.HTTP_400_BAD_REQUEST,
)
cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100)
# Capture Issue Activity
issue_activity.delay(
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": []}),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": "[]",
}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=base_host(request=request, is_app=True),
)
return Response({"message": "Success"}, status=status.HTTP_200_OK)
+22 -18
View File
@@ -987,12 +987,12 @@ class LabelDetailAPIEndpoint(LabelListCreateAPIEndpoint):
serializer = LabelCreateUpdateSerializer(label, data=request.data, partial=True)
if serializer.is_valid():
if (
str(request.data.get("external_id"))
and (label.external_id != str(request.data.get("external_id")))
request.data.get("external_id")
and request.data.get("external_source")
and Label.objects.filter(
project_id=project_id,
workspace__slug=slug,
external_source=request.data.get("external_source", label.external_source),
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
)
.exclude(id=pk)
@@ -1695,23 +1695,27 @@ class IssueActivityDetailAPIEndpoint(BaseAPIView):
Retrieve details of a specific activity.
Excludes comment, vote, reaction, and draft activities.
"""
issue_activities = (
IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
issue_activity = (
(
IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id, id=pk)
.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.filter(project__archived_at__isnull=True)
.select_related("actor", "workspace", "issue", "project")
)
.filter(project__archived_at__isnull=True)
.select_related("actor", "workspace", "issue", "project")
).order_by(request.GET.get("order_by", "created_at"))
.order_by(request.GET.get("order_by", "created_at"))
.first()
)
return self.paginate(
request=request,
queryset=(issue_activities),
on_results=lambda issue_activity: IssueActivitySerializer(
issue_activity, many=True, fields=self.fields, expand=self.expand
).data,
if not issue_activity:
return Response({"message": "Activity not found.", "code": "NOT_FOUND"}, status=status.HTTP_404_NOT_FOUND)
return Response(
IssueActivitySerializer(issue_activity, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
+3 -1
View File
@@ -871,6 +871,8 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
module_id=module_id,
issue_id=issue_id,
)
module_name = module_issue.module.name if module_issue.module is not None else ""
module_issue.delete()
issue_activity.delay(
type="module.activity.deleted",
@@ -878,7 +880,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
current_instance=json.dumps({"module_name": module_name}),
epoch=int(timezone.now().timestamp()),
)
return Response(status=status.HTTP_204_NO_CONTENT)
+1 -1
View File
@@ -79,7 +79,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
)
| Q(network=2)
)
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
.select_related("project_lead")
.annotate(
is_member=Exists(
ProjectMember.objects.filter(
+2
View File
@@ -17,6 +17,7 @@ from .views import urlpatterns as view_urls
from .webhook import urlpatterns as webhook_urls
from .workspace import urlpatterns as workspace_urls
from .timezone import urlpatterns as timezone_urls
from .exporter import urlpatterns as exporter_urls
urlpatterns = [
*analytic_urls,
@@ -38,4 +39,5 @@ urlpatterns = [
*api_urls,
*webhook_urls,
*timezone_urls,
*exporter_urls,
]
+12
View File
@@ -0,0 +1,12 @@
from django.urls import path
from plane.app.views import ExportIssuesEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/export-issues/",
ExportIssuesEndpoint.as_view(),
name="export-issues",
),
]
-7
View File
@@ -7,7 +7,6 @@ from plane.app.views import (
IssueLinkViewSet,
IssueAttachmentEndpoint,
CommentReactionViewSet,
ExportIssuesEndpoint,
IssueActivityEndpoint,
IssueArchiveViewSet,
IssueCommentViewSet,
@@ -141,12 +140,6 @@ urlpatterns = [
IssueAttachmentV2Endpoint.as_view(),
name="project-issue-attachments",
),
## Export Issues
path(
"workspaces/<str:slug>/export-issues/",
ExportIssuesEndpoint.as_view(),
name="export-issues",
),
## End Issues
## Issue Activity
path(
+109 -426
View File
@@ -51,6 +51,7 @@ from plane.db.models import (
from plane.utils.analytics_plot import burndown_plot
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.host import base_host
from plane.utils.cycle_transfer_issues import transfer_cycle_issues
from .. import BaseAPIView, BaseViewSet
from plane.bgtasks.webhook_task import model_activity
from plane.utils.timezone_converter import convert_to_utc, user_timezone_converter
@@ -96,7 +97,9 @@ class CycleViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only("avatar_asset", "first_name", "id").distinct(),
queryset=User.objects.only(
"avatar_asset", "first_name", "id"
).distinct(),
)
)
.prefetch_related(
@@ -147,7 +150,8 @@ class CycleViewSet(BaseViewSet):
.annotate(
status=Case(
When(
Q(start_date__lte=current_time_in_utc) & Q(end_date__gte=current_time_in_utc),
Q(start_date__lte=current_time_in_utc)
& Q(end_date__gte=current_time_in_utc),
then=Value("CURRENT"),
),
When(start_date__gt=current_time_in_utc, then=Value("UPCOMING")),
@@ -166,7 +170,11 @@ class CycleViewSet(BaseViewSet):
"issue_cycle__issue__assignees__id",
distinct=True,
filter=~Q(issue_cycle__issue__assignees__id__isnull=True)
& (Q(issue_cycle__issue__issue_assignee__deleted_at__isnull=True)),
& (
Q(
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
)
@@ -197,7 +205,9 @@ class CycleViewSet(BaseViewSet):
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc)
queryset = queryset.filter(
start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc
)
data = queryset.values(
# necessary fields
@@ -264,10 +274,16 @@ class CycleViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def create(self, request, slug, project_id):
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
if (
request.data.get("start_date", None) is None
and request.data.get("end_date", None) is None
) or (
request.data.get("start_date", None) is not None
and request.data.get("end_date", None) is not None
):
serializer = CycleWriteSerializer(data=request.data, context={"project_id": project_id})
serializer = CycleWriteSerializer(
data=request.data, context={"project_id": project_id}
)
if serializer.is_valid():
serializer.save(project_id=project_id, owned_by=request.user)
cycle = (
@@ -307,7 +323,9 @@ class CycleViewSet(BaseViewSet):
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(cycle, datetime_fields, project_timezone)
cycle = user_timezone_converter(
cycle, datetime_fields, project_timezone
)
# Send the model activity
model_activity.delay(
@@ -323,13 +341,17 @@ class CycleViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
{"error": "Both start date and end date are either required or are to be null"},
{
"error": "Both start date and end date are either required or are to be null"
},
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def partial_update(self, request, slug, project_id, pk):
queryset = self.get_queryset().filter(workspace__slug=slug, project_id=project_id, pk=pk)
queryset = self.get_queryset().filter(
workspace__slug=slug, project_id=project_id, pk=pk
)
cycle = queryset.first()
if cycle.archived_at:
return Response(
@@ -337,21 +359,29 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
)
request_data = request.data
if cycle.end_date is not None and cycle.end_date < timezone.now():
if "sort_order" in request_data:
# Can only change sort order for a completed cycle``
request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
request_data = {
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else:
return Response(
{"error": "The Cycle has already been completed so it cannot be edited"},
{
"error": "The Cycle has already been completed so it cannot be edited"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True, context={"project_id": project_id})
serializer = CycleWriteSerializer(
cycle, data=request.data, partial=True, context={"project_id": project_id}
)
if serializer.is_valid():
serializer.save()
cycle = queryset.values(
@@ -451,7 +481,9 @@ class CycleViewSet(BaseViewSet):
)
if data is None:
return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND)
return Response(
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
)
queryset = queryset.first()
# Fetch the project timezone
@@ -473,7 +505,11 @@ class CycleViewSet(BaseViewSet):
def destroy(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="cycle.activity.deleted",
@@ -524,7 +560,9 @@ class CycleDateCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
start_date = convert_to_utc(date=str(start_date), project_id=project_id, is_start_date=True)
start_date = convert_to_utc(
date=str(start_date), project_id=project_id, is_start_date=True
)
end_date = convert_to_utc(
date=str(end_date),
project_id=project_id,
@@ -597,409 +635,23 @@ class TransferCycleIssueEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first()
old_cycle = (
Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id)
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
)
old_cycle = old_cycle.first()
estimate_type = Project.objects.filter(
workspace__slug=slug,
pk=project_id,
estimate__isnull=False,
estimate__type="points",
).exists()
if estimate_type:
assignee_estimate_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(
avatar_url=Case(
# If `avatar_asset` exists, use it to generate the asset URL
When(
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
Value("/"),
),
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
),
default=Value(None),
output_field=models.CharField(),
)
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
# assignee distribution serialization
assignee_estimate_distribution = [
{
"display_name": item["display_name"],
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
"avatar": item.get("avatar"),
"avatar_url": item.get("avatar_url"),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
}
for item in assignee_estimate_data
]
label_distribution_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
estimate_completion_chart = burndown_plot(
queryset=old_cycle,
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=cycle_id,
)
# Label distribution serialization
label_estimate_distribution = [
{
"label_name": item["label_name"],
"color": item["color"],
"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"],
}
for item in label_distribution_data
]
# Get the assignee distribution
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(
avatar_url=Case(
# If `avatar_asset` exists, use it to generate the asset URL
When(
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
Value("/"),
),
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
default=Value(None),
output_field=models.CharField(),
)
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
.annotate(
completed_issues=Count(
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
# assignee distribution serialized
assignee_distribution_data = [
{
"display_name": item["display_name"],
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
"avatar": item.get("avatar"),
"avatar_url": item.get("avatar_url"),
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
}
for item in assignee_distribution
]
# Get the label distribution
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
.annotate(
completed_issues=Count(
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
# Label distribution serilization
label_distribution_data = [
{
"label_name": item["label_name"],
"color": item["color"],
"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"],
}
for item in label_distribution
]
# Pass the new_cycle queryset to burndown_plot
completion_chart = burndown_plot(
queryset=old_cycle,
# Transfer cycle issues and create progress snapshot
result = transfer_cycle_issues(
slug=slug,
project_id=project_id,
plot_type="issues",
cycle_id=cycle_id,
new_cycle_id=new_cycle_id,
request=request,
user_id=request.user.id,
)
current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first()
current_cycle.progress_snapshot = {
"total_issues": old_cycle.total_issues,
"completed_issues": old_cycle.completed_issues,
"cancelled_issues": old_cycle.cancelled_issues,
"started_issues": old_cycle.started_issues,
"unstarted_issues": old_cycle.unstarted_issues,
"backlog_issues": old_cycle.backlog_issues,
"distribution": {
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
"completion_chart": completion_chart,
},
"estimate_distribution": (
{}
if not estimate_type
else {
"labels": label_estimate_distribution,
"assignees": assignee_estimate_distribution,
"completion_chart": estimate_completion_chart,
}
),
}
current_cycle.save(update_fields=["progress_snapshot"])
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
# Handle error response
if result.get("error"):
return Response(
{"error": "The cycle where the issues are transferred is already completed"},
{"error": result["error"]},
status=status.HTTP_400_BAD_REQUEST,
)
cycle_issues = CycleIssue.objects.filter(
cycle_id=cycle_id,
project_id=project_id,
workspace__slug=slug,
issue__state__group__in=["backlog", "unstarted", "started"],
)
updated_cycles = []
update_cycle_issue_activity = []
for cycle_issue in cycle_issues:
cycle_issue.cycle_id = new_cycle_id
updated_cycles.append(cycle_issue)
update_cycle_issue_activity.append(
{
"old_cycle_id": str(cycle_id),
"new_cycle_id": str(new_cycle_id),
"issue_id": str(cycle_issue.issue_id),
}
)
cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100)
# Capture Issue Activity
issue_activity.delay(
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": []}),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": "[]",
}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=base_host(request=request, is_app=True),
)
return Response({"message": "Success"}, status=status.HTTP_200_OK)
@@ -1014,8 +666,12 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
)
cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
cycle_properties.rich_filters = request.data.get("rich_filters", cycle_properties.rich_filters)
cycle_properties.display_filters = request.data.get("display_filters", cycle_properties.display_filters)
cycle_properties.rich_filters = request.data.get(
"rich_filters", cycle_properties.rich_filters
)
cycle_properties.display_filters = request.data.get(
"display_filters", cycle_properties.display_filters
)
cycle_properties.display_properties = request.data.get(
"display_properties", cycle_properties.display_properties
)
@@ -1039,9 +695,13 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
class CycleProgressEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id).first()
cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, id=cycle_id
).first()
if not cycle:
return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND)
return Response(
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
)
aggregate_estimates = (
Issue.issue_objects.filter(
estimate_point__estimate__type="points",
@@ -1087,7 +747,9 @@ class CycleProgressEndpoint(BaseAPIView):
output_field=FloatField(),
)
),
total_estimate_points=Sum("value_as_float", default=Value(0), output_field=FloatField()),
total_estimate_points=Sum(
"value_as_float", default=Value(0), output_field=FloatField()
),
)
)
if cycle.progress_snapshot:
@@ -1147,11 +809,22 @@ class CycleProgressEndpoint(BaseAPIView):
return Response(
{
"backlog_estimate_points": aggregate_estimates["backlog_estimate_point"] or 0,
"unstarted_estimate_points": aggregate_estimates["unstarted_estimate_point"] or 0,
"started_estimate_points": aggregate_estimates["started_estimate_point"] or 0,
"cancelled_estimate_points": aggregate_estimates["cancelled_estimate_point"] or 0,
"completed_estimate_points": aggregate_estimates["completed_estimate_points"] or 0,
"backlog_estimate_points": aggregate_estimates["backlog_estimate_point"]
or 0,
"unstarted_estimate_points": aggregate_estimates[
"unstarted_estimate_point"
]
or 0,
"started_estimate_points": aggregate_estimates["started_estimate_point"]
or 0,
"cancelled_estimate_points": aggregate_estimates[
"cancelled_estimate_point"
]
or 0,
"completed_estimate_points": aggregate_estimates[
"completed_estimate_points"
]
or 0,
"total_estimate_points": aggregate_estimates["total_estimate_points"],
"backlog_issues": backlog_issues,
"total_issues": total_issues,
@@ -1169,7 +842,9 @@ class CycleAnalyticsEndpoint(BaseAPIView):
def get(self, request, slug, project_id, cycle_id):
analytic_type = request.GET.get("type", "issues")
cycle = (
Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id)
Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, id=cycle_id
)
.annotate(
total_issues=Count(
"issue_cycle__issue__id",
@@ -1196,7 +871,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
if the issues were transferred to the new cycle, then the progress_snapshot will be present
return the progress_snapshot data in the analytics for each date
else issues were not transferred to the new cycle then generate the stats from the cycle isssue bridge tables
else issues were not transferred to the new cycle then generate the stats from the cycle issue bridge tables
"""
if cycle.progress_snapshot:
@@ -1252,7 +927,9 @@ class CycleAnalyticsEndpoint(BaseAPIView):
)
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", FloatField()),
@@ -1287,7 +964,9 @@ class CycleAnalyticsEndpoint(BaseAPIView):
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", FloatField()),
@@ -1389,7 +1068,11 @@ class CycleAnalyticsEndpoint(BaseAPIView):
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id", filter=Q(archived_at__isnull=True, is_draft=False)))
.annotate(
total_issues=Count(
"label_id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
"label_id",
+17 -8
View File
@@ -137,7 +137,11 @@ class PageViewSet(BaseViewSet):
if serializer.is_valid():
serializer.save()
# capture the page transaction
page_transaction.delay(request.data, None, serializer.data["id"])
page_transaction.delay(
new_description_html=request.data.get("description_html", "<p></p>"),
old_description_html=None,
page_id=serializer.data["id"],
)
page = self.get_queryset().get(pk=serializer.data["id"])
serializer = PageDetailSerializer(page)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -168,11 +172,8 @@ class PageViewSet(BaseViewSet):
# capture the page transaction
if request.data.get("description_html"):
page_transaction.delay(
new_value=request.data,
old_value=json.dumps(
{"description_html": page_description},
cls=DjangoJSONEncoder,
),
new_description_html=request.data.get("description_html", "<p></p>"),
old_description_html=page_description,
page_id=page_id,
)
@@ -504,7 +505,11 @@ class PagesDescriptionViewSet(BaseViewSet):
if serializer.is_valid():
# Capture the page transaction
if request.data.get("description_html"):
page_transaction.delay(new_value=request.data, old_value=existing_instance, page_id=page_id)
page_transaction.delay(
new_description_html=request.data.get("description_html", "<p></p>"),
old_description_html=page.description_html,
page_id=page_id,
)
# Update the page using serializer
updated_page = serializer.save()
@@ -550,7 +555,11 @@ class PageDuplicateEndpoint(BaseAPIView):
updated_by_id=page.updated_by_id,
)
page_transaction.delay({"description_html": page.description_html}, None, page.id)
page_transaction.delay(
new_description_html=page.description_html,
old_description_html=None,
page_id=page.id,
)
# Copy the s3 objects uploaded in the page
copy_s3_objects_of_description_and_assets.delay(
+1 -1
View File
@@ -48,7 +48,7 @@ class ProjectMemberViewSet(BaseViewSet):
# Check if the members array is empty
if not len(members):
return Response(
{"error": "Atleast one member is required"},
{"error": "At least one member is required"},
status=status.HTTP_400_BAD_REQUEST,
)
+158 -83
View File
@@ -43,22 +43,25 @@ class GlobalSearchEndpoint(BaseAPIView):
also show related workspace if found
"""
def filter_workspaces(self, query, slug, project_id, workspace_search):
def filter_workspaces(self, query, _slug, _project_id, _workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Workspace.objects.filter(q, workspace_member__member=self.request.user)
.order_by("-created_at")
.distinct()
.values("name", "id", "slug")
)
def filter_projects(self, query, slug, project_id, workspace_search):
def filter_projects(self, query, slug, _project_id, _workspace_search):
fields = ["name", "identifier"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})
return (
Project.objects.filter(
q,
@@ -67,6 +70,7 @@ class GlobalSearchEndpoint(BaseAPIView):
archived_at__isnull=True,
workspace__slug=slug,
)
.order_by("-created_at")
.distinct()
.values("name", "id", "identifier", "workspace__slug")
)
@@ -74,14 +78,15 @@ class GlobalSearchEndpoint(BaseAPIView):
def filter_issues(self, query, slug, project_id, workspace_search):
fields = ["name", "sequence_id", "project__identifier"]
q = Q()
for field in fields:
if field == "sequence_id":
# Match whole integers only (exclude decimal numbers)
sequences = re.findall(r"\b\d+\b", query)
for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": query})
if query:
for field in fields:
if field == "sequence_id":
# Match whole integers only (exclude decimal numbers)
sequences = re.findall(r"\b\d+\b", query)
for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": query})
issues = Issue.issue_objects.filter(
q,
@@ -106,8 +111,9 @@ class GlobalSearchEndpoint(BaseAPIView):
def filter_cycles(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})
cycles = Cycle.objects.filter(
q,
@@ -120,13 +126,20 @@ class GlobalSearchEndpoint(BaseAPIView):
if workspace_search == "false" and project_id:
cycles = cycles.filter(project_id=project_id)
return cycles.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
return (
cycles.order_by("-created_at")
.distinct()
.values(
"name", "id", "project_id", "project__identifier", "workspace__slug"
)
)
def filter_modules(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})
modules = Module.objects.filter(
q,
@@ -139,13 +152,20 @@ class GlobalSearchEndpoint(BaseAPIView):
if workspace_search == "false" and project_id:
modules = modules.filter(project_id=project_id)
return modules.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
return (
modules.order_by("-created_at")
.distinct()
.values(
"name", "id", "project_id", "project__identifier", "workspace__slug"
)
)
def filter_pages(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})
pages = (
Page.objects.filter(
@@ -157,7 +177,9 @@ class GlobalSearchEndpoint(BaseAPIView):
)
.annotate(
project_ids=Coalesce(
ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
ArrayAgg(
"projects__id", distinct=True, filter=~Q(projects__id=True)
),
Value([], output_field=ArrayField(UUIDField())),
)
)
@@ -174,19 +196,28 @@ class GlobalSearchEndpoint(BaseAPIView):
)
if workspace_search == "false" and project_id:
project_subquery = ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=project_id).values_list(
"project_id", flat=True
)[:1]
project_subquery = ProjectPage.objects.filter(
page_id=OuterRef("id"), project_id=project_id
).values_list("project_id", flat=True)[:1]
pages = pages.annotate(project_id=Subquery(project_subquery)).filter(project_id=project_id)
pages = pages.annotate(project_id=Subquery(project_subquery)).filter(
project_id=project_id
)
return pages.distinct().values("name", "id", "project_ids", "project_identifiers", "workspace__slug")
return (
pages.order_by("-created_at")
.distinct()
.values(
"name", "id", "project_ids", "project_identifiers", "workspace__slug"
)
)
def filter_views(self, query, slug, project_id, workspace_search):
fields = ["name"]
q = Q()
for field in fields:
q |= Q(**{f"{field}__icontains": query})
if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})
issue_views = IssueView.objects.filter(
q,
@@ -199,29 +230,57 @@ class GlobalSearchEndpoint(BaseAPIView):
if workspace_search == "false" and project_id:
issue_views = issue_views.filter(project_id=project_id)
return issue_views.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
return (
issue_views.order_by("-created_at")
.distinct()
.values(
"name", "id", "project_id", "project__identifier", "workspace__slug"
)
)
def filter_intakes(self, query, slug, project_id, workspace_search):
fields = ["name", "sequence_id", "project__identifier"]
q = Q()
if query:
for field in fields:
if field == "sequence_id":
# Match whole integers only (exclude decimal numbers)
sequences = re.findall(r"\b\d+\b", query)
for sequence_id in sequences:
q |= Q(**{"sequence_id": sequence_id})
else:
q |= Q(**{f"{field}__icontains": query})
issues = Issue.objects.filter(
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
workspace__slug=slug,
).filter(models.Q(issue_intake__status=0) | models.Q(issue_intake__status=-2))
if workspace_search == "false" and project_id:
issues = issues.filter(project_id=project_id)
return (
issues.order_by("-created_at")
.distinct()
.values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"workspace__slug",
)[:100]
)
def get(self, request, slug):
query = request.query_params.get("search", False)
entities_param = request.query_params.get("entities")
workspace_search = request.query_params.get("workspace_search", "false")
project_id = request.query_params.get("project_id", False)
if not query:
return Response(
{
"results": {
"workspace": [],
"project": [],
"issue": [],
"cycle": [],
"module": [],
"issue_view": [],
"page": [],
}
},
status=status.HTTP_200_OK,
)
MODELS_MAPPER = {
"workspace": self.filter_workspaces,
"project": self.filter_projects,
@@ -230,13 +289,27 @@ class GlobalSearchEndpoint(BaseAPIView):
"module": self.filter_modules,
"issue_view": self.filter_views,
"page": self.filter_pages,
"intake": self.filter_intakes,
}
# Determine which entities to search
if entities_param:
requested_entities = [
e.strip() for e in entities_param.split(",") if e.strip()
]
requested_entities = [e for e in requested_entities if e in MODELS_MAPPER]
else:
requested_entities = list(MODELS_MAPPER.keys())
results = {}
for model in MODELS_MAPPER.keys():
func = MODELS_MAPPER.get(model, None)
results[model] = func(query, slug, project_id, workspace_search)
for entity in requested_entities:
func = MODELS_MAPPER.get(entity)
if func:
results[entity] = func(
query or None, slug, project_id, workspace_search
)
return Response({"results": results}, status=status.HTTP_200_OK)
@@ -294,29 +367,15 @@ class SearchEndpoint(BaseAPIView):
.order_by("-created_at")
)
if issue_id:
issue_created_by = (
Issue.objects.filter(id=issue_id).values_list("created_by_id", flat=True).first()
)
users = (
users.filter(Q(role__gt=10) | Q(member_id=issue_created_by))
.distinct()
.values(
"member__avatar_url",
"member__display_name",
"member__id",
)
)
else:
users = (
users.filter(Q(role__gt=10))
.distinct()
.values(
"member__avatar_url",
"member__display_name",
"member__id",
)
users = (
users
.distinct()
.values(
"member__avatar_url",
"member__display_name",
"member__id",
)
)
response_data["user_mention"] = list(users[:count])
@@ -330,12 +389,15 @@ class SearchEndpoint(BaseAPIView):
projects = (
Project.objects.filter(
q,
Q(project_projectmember__member=self.request.user) | Q(network=2),
Q(project_projectmember__member=self.request.user)
| Q(network=2),
workspace__slug=slug,
)
.order_by("-created_at")
.distinct()
.values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
.values(
"name", "id", "identifier", "logo_props", "workspace__slug"
)[:count]
)
response_data["project"] = list(projects)
@@ -394,16 +456,20 @@ class SearchEndpoint(BaseAPIView):
.annotate(
status=Case(
When(
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
Q(start_date__lte=timezone.now())
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(
start_date__gt=timezone.now(),
then=Value("UPCOMING"),
),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
end_date__lt=timezone.now(), then=Value("COMPLETED")
),
When(
Q(start_date__isnull=True)
& Q(end_date__isnull=True),
then=Value("DRAFT"),
),
default=Value("DRAFT"),
@@ -521,7 +587,9 @@ class SearchEndpoint(BaseAPIView):
)
)
.order_by("-created_at")
.values("member__avatar_url", "member__display_name", "member__id")[:count]
.values(
"member__avatar_url", "member__display_name", "member__id"
)[:count]
)
response_data["user_mention"] = list(users)
@@ -535,12 +603,15 @@ class SearchEndpoint(BaseAPIView):
projects = (
Project.objects.filter(
q,
Q(project_projectmember__member=self.request.user) | Q(network=2),
Q(project_projectmember__member=self.request.user)
| Q(network=2),
workspace__slug=slug,
)
.order_by("-created_at")
.distinct()
.values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
.values(
"name", "id", "identifier", "logo_props", "workspace__slug"
)[:count]
)
response_data["project"] = list(projects)
@@ -597,16 +668,20 @@ class SearchEndpoint(BaseAPIView):
.annotate(
status=Case(
When(
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
Q(start_date__lte=timezone.now())
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(
start_date__gt=timezone.now(),
then=Value("UPCOMING"),
),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
end_date__lt=timezone.now(), then=Value("COMPLETED")
),
When(
Q(start_date__isnull=True)
& Q(end_date__isnull=True),
then=Value("DRAFT"),
),
default=Value("DRAFT"),
+1 -1
View File
@@ -99,7 +99,7 @@ class TimezoneEndpoint(APIView):
("Tunis", "Africa/Tunis"), # UTC+01:00
(
"Eastern European Time (Cairo, Helsinki, Kyiv)",
"Europe/Kiev",
"Europe/Kyiv",
), # UTC+02:00 (DST: UTC+03:00)
("Athens", "Europe/Athens"), # UTC+02:00 (DST: UTC+03:00)
("Jerusalem", "Asia/Jerusalem"), # UTC+02:00 (DST: UTC+03:00)
@@ -38,9 +38,11 @@ AUTHENTICATION_ERROR_CODES = {
"GITHUB_NOT_CONFIGURED": 5110,
"GITHUB_USER_NOT_IN_ORG": 5122,
"GITLAB_NOT_CONFIGURED": 5111,
"GITEA_NOT_CONFIGURED": 5112,
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
"GITEA_OAUTH_PROVIDER_ERROR": 5123,
# Reset Password
"INVALID_PASSWORD_TOKEN": 5125,
"EXPIRED_PASSWORD_TOKEN": 5130,
@@ -48,7 +50,7 @@ AUTHENTICATION_ERROR_CODES = {
"INCORRECT_OLD_PASSWORD": 5135,
"MISSING_PASSWORD": 5138,
"INVALID_NEW_PASSWORD": 5140,
# set passowrd
# set password
"PASSWORD_ALREADY_SET": 5145,
# Admin
"ADMIN_ALREADY_EXIST": 5150,
@@ -48,6 +48,8 @@ class OauthAdapter(Adapter):
return "GITHUB_OAUTH_PROVIDER_ERROR"
elif self.provider == "gitlab":
return "GITLAB_OAUTH_PROVIDER_ERROR"
elif self.provider == "gitea":
return "GITEA_OAUTH_PROVIDER_ERROR"
else:
return "OAUTH_NOT_CONFIGURED"
@@ -0,0 +1,171 @@
import os
from datetime import datetime, timedelta
from urllib.parse import urlencode, urlparse
import pytz
import requests
# Module imports
from plane.authentication.adapter.oauth import OauthAdapter
from plane.license.utils.instance_value import get_configuration_value
from plane.authentication.adapter.error import (
AUTHENTICATION_ERROR_CODES,
AuthenticationException,
)
class GiteaOAuthProvider(OauthAdapter):
provider = "gitea"
scope = "openid email profile"
def __init__(self, request, code=None, state=None, callback=None):
(GITEA_CLIENT_ID, GITEA_CLIENT_SECRET, GITEA_HOST) = get_configuration_value(
[
{
"key": "GITEA_CLIENT_ID",
"default": os.environ.get("GITEA_CLIENT_ID"),
},
{
"key": "GITEA_CLIENT_SECRET",
"default": os.environ.get("GITEA_CLIENT_SECRET"),
},
{
"key": "GITEA_HOST",
"default": os.environ.get("GITEA_HOST"),
},
]
)
if not (GITEA_CLIENT_ID and GITEA_CLIENT_SECRET and GITEA_HOST):
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITEA_NOT_CONFIGURED"],
error_message="GITEA_NOT_CONFIGURED",
)
# Enforce scheme and normalize trailing slash(es)
parsed = urlparse(GITEA_HOST)
if not parsed.scheme or parsed.scheme not in ("https", "http"):
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITEA_NOT_CONFIGURED"],
error_message="GITEA_NOT_CONFIGURED", # avoid leaking details to query params
)
GITEA_HOST = GITEA_HOST.rstrip("/")
# Set URLs based on the host
self.token_url = f"{GITEA_HOST}/login/oauth/access_token"
self.userinfo_url = f"{GITEA_HOST}/api/v1/user"
client_id = GITEA_CLIENT_ID
client_secret = GITEA_CLIENT_SECRET
redirect_uri = f"{'https' if request.is_secure() else 'http'}://{request.get_host()}/auth/gitea/callback/"
url_params = {
"client_id": client_id,
"scope": self.scope,
"redirect_uri": redirect_uri,
"response_type": "code",
"state": state,
}
auth_url = f"{GITEA_HOST}/login/oauth/authorize?{urlencode(url_params)}"
super().__init__(
request,
self.provider,
client_id,
self.scope,
redirect_uri,
auth_url,
self.token_url,
self.userinfo_url,
client_secret,
code,
callback=callback,
)
def set_token_data(self):
data = {
"code": self.code,
"client_id": self.client_id,
"client_secret": self.client_secret,
"redirect_uri": self.redirect_uri,
"grant_type": "authorization_code",
}
headers = {"Accept": "application/json"}
token_response = self.get_user_token(data=data, headers=headers)
super().set_token_data(
{
"access_token": token_response.get("access_token"),
"refresh_token": token_response.get("refresh_token", None),
"access_token_expired_at": (
datetime.now(tz=pytz.utc) + timedelta(seconds=token_response.get("expires_in"))
if token_response.get("expires_in")
else None
),
"refresh_token_expired_at": (
datetime.fromtimestamp(
token_response.get("refresh_token_expired_at"), tz=pytz.utc
)
if token_response.get("refresh_token_expired_at")
else None
),
"id_token": token_response.get("id_token", ""),
}
)
def __get_email(self, headers):
try:
# Gitea may not provide email in user response, so fetch it separately
emails_url = f"{self.userinfo_url}/emails"
response = requests.get(emails_url, headers=headers)
if not response.ok:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
error_message="GITEA_OAUTH_PROVIDER_ERROR: Failed to fetch emails",
)
emails_response = response.json()
if not emails_response:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
error_message="GITEA_OAUTH_PROVIDER_ERROR: No emails found",
)
# Prefer primary+verified, then any verified, then primary, else first
email = next((e.get("email") for e in emails_response if e.get("primary") and e.get("verified")), None)
if not email:
email = next((e.get("email") for e in emails_response if e.get("verified")), None)
if not email:
email = next((e.get("email") for e in emails_response if e.get("primary")), None)
if not email and emails_response:
# If no primary email, use the first one
email = emails_response[0].get("email")
return email
except requests.RequestException:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITEA_OAUTH_PROVIDER_ERROR"],
error_message="GITEA_OAUTH_PROVIDER_ERROR: Exception occurred while fetching emails",
)
def set_user_data(self):
user_info_response = self.get_user_response()
headers = {
"Authorization": f"Bearer {self.token_data.get('access_token')}",
"Accept": "application/json",
}
# Get email if not provided in user info
email = user_info_response.get("email")
if not email:
email = self.__get_email(headers=headers)
super().set_user_data(
{
"email": email,
"user": {
"provider_id": str(user_info_response.get("id")),
"email": email,
"avatar": user_info_response.get("avatar_url"),
"first_name": user_info_response.get("full_name") or user_info_response.get("login"),
"last_name": "", # Gitea doesn't provide separate first/last name
"is_password_autoset": True,
},
}
)
+17
View File
@@ -36,6 +36,10 @@ from .views import (
SignInAuthSpaceEndpoint,
SignUpAuthSpaceEndpoint,
SignOutAuthSpaceEndpoint,
GiteaCallbackEndpoint,
GiteaOauthInitiateEndpoint,
GiteaCallbackSpaceEndpoint,
GiteaOauthInitiateSpaceEndpoint,
)
urlpatterns = [
@@ -129,4 +133,17 @@ urlpatterns = [
),
path("change-password/", ChangePasswordEndpoint.as_view(), name="forgot-password"),
path("set-password/", SetUserPasswordEndpoint.as_view(), name="set-password"),
## Gitea Oauth
path("gitea/", GiteaOauthInitiateEndpoint.as_view(), name="gitea-initiate"),
path("gitea/callback/", GiteaCallbackEndpoint.as_view(), name="gitea-callback"),
path(
"spaces/gitea/",
GiteaOauthInitiateSpaceEndpoint.as_view(),
name="space-gitea-initiate",
),
path(
"spaces/gitea/callback/",
GiteaCallbackSpaceEndpoint.as_view(),
name="space-gitea-callback",
),
]
@@ -5,6 +5,7 @@ from .app.check import EmailCheckEndpoint
from .app.email import SignInAuthEndpoint, SignUpAuthEndpoint
from .app.github import GitHubCallbackEndpoint, GitHubOauthInitiateEndpoint
from .app.gitlab import GitLabCallbackEndpoint, GitLabOauthInitiateEndpoint
from .app.gitea import GiteaCallbackEndpoint, GiteaOauthInitiateEndpoint
from .app.google import GoogleCallbackEndpoint, GoogleOauthInitiateEndpoint
from .app.magic import MagicGenerateEndpoint, MagicSignInEndpoint, MagicSignUpEndpoint
@@ -17,6 +18,8 @@ from .space.github import GitHubCallbackSpaceEndpoint, GitHubOauthInitiateSpaceE
from .space.gitlab import GitLabCallbackSpaceEndpoint, GitLabOauthInitiateSpaceEndpoint
from .space.gitea import GiteaCallbackSpaceEndpoint, GiteaOauthInitiateSpaceEndpoint
from .space.google import GoogleCallbackSpaceEndpoint, GoogleOauthInitiateSpaceEndpoint
from .space.magic import (

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