Compare commits

...

147 Commits

Author SHA1 Message Date
Anmol Singh Bhatia
d021d38a98 chore: issue tooltip bug fix 2023-11-07 17:59:38 +05:30
Anmol Singh Bhatia
3d5cbaae2a chore: duplicate label alert added 2023-11-07 17:59:00 +05:30
Manish Gupta
f7cc2eca36 dev: Modified the branch-build action yaml (#2704)
* cherrypicked code

* removed PUSH event
2023-11-07 17:41:26 +05:30
Prateek Shourya
63d1ad286b style: update font weight in project general setting section. (#2697) 2023-11-07 17:39:12 +05:30
Manish Gupta
1412c1c94a dev: modified the branch wise build (#2702)
* cherrypicked branch build code

* trigger on pull request

* branch filter

* checking branch filter

* checking push

* checking push again

* code cleanup before PR
2023-11-07 17:23:32 +05:30
sriram veeraghanta
26de35bd8d fix: environment config changes in the API are replicated in web and space app (#2699)
* fix: envconfig type changes

* chore: configuration variables  (#2692)

* chore: update avatar group logic (#2672)

* chore: configuration variables

---------

* fix: replacing slack client id with env config

---------

Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
2023-11-07 17:17:10 +05:30
Aaryan Khandelwal
1986c0dfd4 fix: state icon (#2678) 2023-11-07 16:15:34 +05:30
Anmol Singh Bhatia
25f3a5b2e4 chore: peek overview improvement and bug fixes (#2683)
* style: issue peek overview improvement

* style: peek overview improvement

* fix: subscribe issue from peek overview fix and validation added

* fix: build error
2023-11-07 15:58:42 +05:30
Anmol Singh Bhatia
f30b16e9d8 chore: user profile issue improvement (#2679)
* fix: user profile filters z-index

* chore: user profile issue state group heading fix

* fix: build error
2023-11-07 15:58:19 +05:30
Anmol Singh Bhatia
93d03f82b4 chore: spreadsheet layout improvement (#2677)
* style: spreadsheet column width fix

* style: spreadsheet label column styling

* chore: spreadsheet layout issue properties improvement

* fix: build error
2023-11-07 15:58:00 +05:30
Anmol Singh Bhatia
98974fdc50 fix: cycle and module bug fixes and improvement (#2691)
* fix: cycle and module card issue count fix

* fix: cycle and module list progress icon fix

* fix: module card progress fix

* style: cycle & module empty date label updated

* fix: build error
2023-11-07 15:14:47 +05:30
sriram veeraghanta
040563d148 fix: replacing jira importer image (#2685) 2023-11-07 14:35:04 +05:30
Nikhil
4de64f112f fix: slack project integration (#2684) 2023-11-07 14:34:30 +05:30
Ramesh Kumar Chandra
0afb900678 fix: kanban card state name and drop down items text overflow (#2686) 2023-11-07 14:31:29 +05:30
Prateek Shourya
baf17a109b style: update project description as per design. (#2682) 2023-11-07 13:13:14 +05:30
Prateek Shourya
37bf465fcd style: update border across workspace and project settings. (#2669)
* style: update border across workspace and project settings.

* update border width
2023-11-07 13:12:05 +05:30
Anmol Singh Bhatia
d8c96536f0 fix: bug fixes and ui improvement (#2674)
* chore: peekoverview edit permission updated

* chore: tab index added in create project modal

* chore: project card improvement

* style: avatar component improvement

* chore: create issue modal improvement

* style: global style sidebar border variable name fix
2023-11-06 21:08:01 +05:30
Nikhil
b372ccfdb3 fix: slack integration workflow (#2675)
* fix: slack integration workflow

* dev: add slack client id as configuration

* fix: clean up

* fix: added env to turbo

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-06 21:00:49 +05:30
guru_sainath
984b36f45a fix: In kanban issues can be shifted between the column in order_by (#2676) 2023-11-06 21:00:36 +05:30
Aaryan Khandelwal
46f307fed5 chore: update avatar group logic (#2672) 2023-11-06 20:43:54 +05:30
Aaryan Khandelwal
1dce72cb3c style: updated layouts UI in the space app (#2671)
* style: updated layouts UI in space

* fix: build error
2023-11-06 20:43:34 +05:30
Aaryan Khandelwal
a6dea3af23 fix: render the estimate select if estimate is enabled for the project (#2663) 2023-11-06 20:43:10 +05:30
Henit Chobisa
6eb0bf4785 Fix/mentions spaces fix (#2667)
* feat: add mentions store to the space project

* fix: added mentions highlights in read only comment cards

* feat: added mention highlights in richtexteditor in space app
2023-11-06 20:42:24 +05:30
Manish Gupta
13389d1b2b dev: On Demand Code Build for any branch (#2668)
* wip

* wip

* testing

* wip

* wip

* wip

* wip

* image push fix

* wip

* wip

* dynamic branch name and tag

* workflow_dispatch modified

* job splitting

* file sharing

* wip

* checking

* wip

* wip

* wip

* wip

* build fixes

* code upload download fixes

* image name change

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-06 19:05:20 +05:30
Aaryan Khandelwal
742143766f fix: existing issues modal for cycle and module (#2664)
* fix: existing issues modal for cycle and module

* refactor: existing issues modal code

* fix: build errors
2023-11-06 16:30:09 +05:30
sriram veeraghanta
1ed72c51df fix: package version fixes and mentions build error fixes (#2665) 2023-11-06 16:28:15 +05:30
Aaryan Khandelwal
a03e0c788f fix: notifications option in the sidebar menu not collapsing (#2662) 2023-11-06 14:53:26 +05:30
guru_sainath
0c8a867565 fix: handled drag and drop issue, gantt hover issue for issue peek overview (#2660) 2023-11-06 13:52:33 +05:30
Aaryan Khandelwal
3a07bb6060 refactor: removed unused packages (#2658) 2023-11-06 13:17:02 +05:30
Aaryan Khandelwal
bf48d93a25 fix: product tour modal bugs (#2657)
* fix: product tour

* style: product tour navigation buttons

* refactor: step logic
2023-11-06 13:06:00 +05:30
M. Palanikannan
14ac885e55 [feat]: Extended Tables Support (#2596)
* migrated table to new project structure

* fixed range errors while deleting table nodes with no nodes below and removed console logs

* fixed css for rendering table menu

* removed old table menu

* added support for read only editors as well

* text-black removed

* added design colors

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-05 18:54:00 +05:30
Henit Chobisa
f0335751b3 fix: mentions enter error (#2646)
* fix: fixed readonly lite text editor not rendering highlights

* fix: removed enter extension in lite text editor
2023-11-04 01:57:57 +05:30
Anmol Singh Bhatia
52395d0563 chore : dropdown loading state added and project card avatar fix (#2643)
* chore: project card avatar rendering fix

* chore: state, assignee and label dropdown loading state added
2023-11-04 01:56:23 +05:30
Dakshesh Jain
ad558833af refactor: archive issue (#2628)
* dev: archived issue store

* dev: archived issue layouts and store binding

* dev: archived issue detail store

* dev: is read only

* fix: archived issue delete

* fix: build error
2023-11-03 20:20:05 +05:30
sriram veeraghanta
ff258c60fd fix: open changelog in new tab (#2645) 2023-11-03 19:49:02 +05:30
Bavisetti Narayan
db2a1b8033 fix: custom analytics graph display issue (#2637)
* chore: fixed custom analytics

* chore: typo changes
2023-11-03 19:23:35 +05:30
Dakshesh Jain
91878fb3dd fix: notification read and snooze bugs (#2639)
* fix: marking notification as read doesn't remove it from un-read list

* refactor: arranged imports

* fix: past snooze notifications coming in snooze tab
2023-11-03 19:21:35 +05:30
Lakhan Baheti
c233e6e3b6 style: custom analytics bar graph label overlapping (#2636)
* style: custom analytics bar graph label overlapping

fix: Bar graph avatar image rendering & tooltip

* fix: import
2023-11-03 19:18:24 +05:30
Lakhan Baheti
0d2c399555 style: quick add issue UI improvements in all the layouts (#2615)
* style: quick add issue UI improvements in all the layouts

* style: ui improvements

* style: quick add icon size

* chore: static sizes to tailwind classes

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-11-03 19:17:50 +05:30
Aaryan Khandelwal
79cad16aba chore: update all layout selections (#2641) 2023-11-03 19:17:13 +05:30
Aaryan Khandelwal
41e9d5d7e3 chore: added missing columns to the spreadsheet layout (#2640) 2023-11-03 19:15:09 +05:30
Aaryan Khandelwal
992cf79031 chore: peek overview authorization (#2632)
* chore: peek overview authorization

* chore: comment access specifier validation
2023-11-03 19:13:10 +05:30
Aaryan Khandelwal
d48f13416f Update readme content (#2635) 2023-11-03 19:12:46 +05:30
Aaryan Khandelwal
cf19afa707 fix: string helper function (#2633) 2023-11-03 19:11:28 +05:30
sriram veeraghanta
cc26f604aa fix: next image fixes for selfhosted instances (#2642) 2023-11-03 19:09:40 +05:30
Anmol Singh Bhatia
8919b724c5 chore: breadcrumbs ui revamp and refactor (#2634) 2023-11-03 18:01:49 +05:30
Anmol Singh Bhatia
7eeac188d7 chore: peek overview improvement and bug fixes (#2627)
* chore: peekoverview issue properties text size fix

* chore: peekoverview icon updated and active view indicator added

* chore: peekoverview and issue sidebar improvement
2023-11-03 18:01:34 +05:30
Anmol Singh Bhatia
f639e467f8 refactor: replace ui components with plane ui components (#2626)
* refactor: replace button component with plane ui component and remove old button component

* refactor: replace dropdown component with plane ui component

* refactor: replace tooltip, input, textarea, spinner and loader component with plane ui component

* refactor: plane ui code refactor
2023-11-03 17:21:38 +05:30
Anmol Singh Bhatia
737fea28c6 chore: refactor and improve project member settings (#2625)
* fix: project member setting improvement and refactor

* fix: typo fix in automations setting
2023-11-03 17:20:49 +05:30
Anmol Singh Bhatia
4c1aee0cfc fix: resolve z-index and peek overview component bug (#2624)
* fix: resolved z-index issue on peek overview component

* fix: fix issue with peekover view in spreadsheet view

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-11-03 17:20:14 +05:30
guru_sainath
1352c200dd fix: Project Rendering Error in Kanban Layout and Layout Rendering Fixes in Subscribed Profile Issues (#2629)
* fix: rendering projects error in kanabn layout in profile issues and resolved multiplr layout rendering in subscribed profile issues

* fix: implemented spinner loader in profile issues and remove logs in kanban layout
2023-11-03 13:17:52 +05:30
Aaryan Khandelwal
dd2ba2ec6f fix: slug field not working (#2622) 2023-11-03 13:17:01 +05:30
Aaryan Khandelwal
c66d76df26 chore: set sub group by to null if group by and sub group by are same (#2621) 2023-11-03 13:15:50 +05:30
Bavisetti Narayan
7a11161cd0 dev: migrations for 0.14 (#2630)
* chore: migration files

* dev: deleted the old migration
2023-11-03 13:00:37 +05:30
Aaryan Khandelwal
260974b0de fix: user authentication on the index page (#2619)
* fix: user authentication on the index page

* fix: login redirection cleanup

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-03 12:42:43 +05:30
sriram veeraghanta
5efc6993cd fix: Update analytics page layout fixes (#2623) 2023-11-03 00:09:13 +05:30
sriram veeraghanta
3c884fd46e fix: implementing layouts using _app.tsx get layout method. (#2620)
* fix: implementing layouts in all pages

* fix: layout fixes, implemting using standard nextjs parctice
2023-11-02 23:57:44 +05:30
Anmol Singh Bhatia
a582021f2c fix: active cycle fix (#2618) 2023-11-02 22:17:10 +05:30
Bavisetti Narayan
caca2bb548 chore: bug fixes (#2609)
* chore: sub issue activity task

* fix: mentions and issue comment

* chore: added string for issue

* chore: changed sub issue id
2023-11-02 19:32:44 +05:30
Henit Chobisa
da391064aa [FIX] Minor bug fixes in MentionList and MentionNode UI (#2600)
* fix: removed text color in peek view

* fix: fixed list view UI bugs and node view colors

* feat: update imports in suggestions for mentionSuggestion type

* fix: updated mention list css

* fix: updated mention node UI according to the design provided

* style: update the mentions dropdown UI

* style: mentioned users UI in the editor

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-11-02 19:31:25 +05:30
Aaryan Khandelwal
a9b72fa1d2 chore: implemented MobX in the onboarding screens (#2617)
* fix: wrap the onboarding route with the UserWrapper

* chore: implement mobx in the onboarding screens
2023-11-02 19:27:25 +05:30
Anmol Singh Bhatia
b0397dfd74 fix: module sidebar status select (#2614) 2023-11-02 18:48:57 +05:30
guru_sainath
02d4d32f7a chore: Improved Handling of Empty Properties for Labels and Assignees (#2616)
* fix: show empty group

* chore: handled None values for lables and assignees in list and kanban layouts

---------

Co-authored-by: dakshesh14 <dakshesh.jain14@gmail.com>
2023-11-02 18:44:02 +05:30
Anmol Singh Bhatia
43e42f1896 fix: project view item edit action fix (#2612) 2023-11-02 17:12:04 +05:30
Aaryan Khandelwal
d5fd69354e chore: removed unused hooks and components (#2611)
* chore: remove unused hooks

* chore: removed useProjectMembers hook

* chore: removed issue hooks

* fix: build errors
2023-11-02 17:11:33 +05:30
Aaryan Khandelwal
c987c6f308 fix: calendar layout not being rendered (#2610) 2023-11-02 17:08:48 +05:30
Anmol Singh Bhatia
56e4152756 fix: select label dropdown fix for list view (#2608) 2023-11-02 16:28:07 +05:30
Aaryan Khandelwal
7b5ed252ef chore: updated the contact email (#2605)
Updated the contact email to squawk@plane.so
2023-11-02 16:27:23 +05:30
Aaryan Khandelwal
c394a4f64e style: lite text editor editor toolbar (#2601)
* style: comment editor toolbar

* style: updated icon styling
2023-11-02 16:26:57 +05:30
Dakshesh Jain
5b808571e5 fix: exception error (#2606)
* fix: exception error

* fix: invitation type
2023-11-02 16:26:16 +05:30
Dakshesh Jain
2cda47dc8a refactor: user profile store setup and bug fixes (#2586)
* fix: autorun not working when filters are changed

* fix: filter/display on overview page

* refactor: store implementation & loader in 'created' & 'subscribed' page
2023-11-02 16:25:44 +05:30
Anmol Singh Bhatia
7f3dbe298c fix: bug fixes (#2607)
* fix: module card issue count fix

* fix: project kanban view add issue bug fix

* fix: draft issue modal button alignment fix
2023-11-02 16:03:03 +05:30
Anmol Singh Bhatia
0072160891 fix: peekoverview (#2603)
* fix: peekoverview mutation fix

* fix: peekoverview mutation fix

* fix: sub-issue peekoverview
2023-11-02 16:02:34 +05:30
Anmol Singh Bhatia
4512651f8b fix: spreadsheet view properties fix (#2599) 2023-11-02 16:01:49 +05:30
guru_sainath
f6b95b8d31 fix: Issue properties dropdown overflow issue for date and labels (#2604) 2023-11-02 15:59:43 +05:30
Anmol Singh Bhatia
325fb4a377 chore: fixes and improvement (#2595)
* fix: project card fix

* chore: bug fixes and ui improvement
2023-11-02 14:01:56 +05:30
guru_sainath
ba7b7d6f8b chore: implemented module and cycle select dropdown in issue create modal (#2602) 2023-11-02 13:55:45 +05:30
Nikhil
7249f84e18 dev: code improvements and minor performance upgrades (#2201)
* dev: remove len for empty comparison

* dev: using in instead of multiple ors

* dev: assign expression to empty variables

* dev: use f-string

* dev: remove list comprehension and use generators

* dev: remove assert from paginator

* dev: use is for identity comparison with singleton

* dev: remove unnecessary else statements

* dev: fix does not exists error for both project and workspace

* dev: remove reimports

* dev: iterate a dictionary

* dev: remove unused commented code

* dev: remove redefinition

* dev: remove unused imports

* dev: remove unused imports

* dev: remove unnecessary f strings

* dev: remove unused variables

* dev: use literal structure to create the data structure

* dev: add empty lines at the end of the file

* dev: remove user middleware

* dev: remove unnecessary default None
2023-11-01 20:35:06 +05:30
Aaryan Khandelwal
d63e7cf254 chore: filters view more and less buttons (#2583)
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-01 20:34:02 +05:30
Aaryan Khandelwal
36152ea2fa chore: loading state for all layouts (#2588)
* chore: add loading states to layouts

* chore: don't show count for 0 inbox issues
2023-11-01 20:24:57 +05:30
Lakhan Baheti
1a46c6c399 feat: search project & workspace members (#2590)
* feat: search project & workspace members

* chore: formatting
2023-11-01 20:23:21 +05:30
Bavisetti Narayan
4f09a89f5e chore: unarchived issue and date format changes (#2598)
* chore: unarchived issue message corrected

* chore: passing the date in archived at
2023-11-01 20:11:40 +05:30
sriram veeraghanta
8c620c4f96 fix: store level fixes (#2597) 2023-11-01 19:22:10 +05:30
Anmol Singh Bhatia
d46eb9c59a chore: add issue option added in header group (#2592)
* chore: add issue option added in list view group header

* chore: add issue option added in kanban view group header
2023-11-01 19:10:29 +05:30
Bavisetti Narayan
e9321a66e7 chore: added validation for archived issue (#2593)
* chore: added validation for archived issue

* fix: optimised code
2023-11-01 17:20:55 +05:30
sriram veeraghanta
0121a4ab51 [FED-594] fix: user change theme interface bug fixes (#2587)
* fix: user change theme interface bugfixes

* fix: handling error case
2023-11-01 17:11:29 +05:30
Anmol Singh Bhatia
548e95c7e0 fix: bug fixes (#2581)
* fix: module sidebar fix for kanban layout

* chore: cycle & module sidebar improvement

* chore: join project content updated

* chore: project empty state header fix

* chore: create project modal dropdown consistency

* chore: list view group header overlapping issue fix

* chore: popover code refactor

* chore: module sidebar fix for cycle kanban view

* chore: add existing issue option added in module empty state

* chore: add existing issue option added in cycle empty state
2023-11-01 17:11:07 +05:30
Aaryan Khandelwal
13ead7c314 fix: project wrapper (#2589)
* fix: project wrapper

* fix: project wrapper for unjoined project

* chore: update store structure
2023-11-01 17:10:10 +05:30
sriram veeraghanta
4fcc4b4a01 fix: build fixes (#2591) 2023-11-01 16:56:44 +05:30
Henit Chobisa
d511799f31 [FEATURE] Enabled User @mentions and @mention-filters in core editor package (#2544)
* feat: created custom mention component

* feat: added mention suggestions and suggestion highlights

* feat: created mention suggestion list for displaying mention suggestions

* feat: created custom mention text component, for handling click event

* feat: exposed mention component

* feat: integrated and exposed `mentions` componenet with `editor-core`

* feat: integrated mentions extension with the core editor package

* feat: exposed suggestion types from mentions

* feat: added `mention-suggestion` parameters in `r-t-e` and `l-t-e`

* feat: added `IssueMention` model in apiserver models

* chore: updated activities background job and added bs4 in requirements

* feat: added mention removal logic in issue_activity

* chore: exposed mention types from `r-t-e` and `l-t-e`

* feat: integrated mentions in side peek view description form

* feat: added mentions in issue modal form

* feat: created custom react-hook for editor suggestions

* feat: integrated mention suggestions block in RichTextEditor

* feat: added `mentions` integration in `lite-text-editor` instances

* fix: tailwind loading nodemodules from packages

* feat: added styles for the mention suggestion list

* fix: update module import to resolve build failure

* feat: added mentions as an issue filter

* feat: added UI Changes to Implement `mention` filters

* feat: added `mentions` as a filter option in the header

* feat: added mentions in the filter list options

* feat: added mentions in default display filter options

* feat: added filters in applied and issue params in store

* feat: modified types for adding mentions as a filter option

* feat: modified `notification-card` to display message when it exists in object

* feat: rewrote user mention management upon the changes made in develop

* chore: merged debounce PR with the current PR for tracing changes

* fix: mentions_filters updated with the new setup

* feat: updated requirements for bs4

* feat: modified `mentions-filter` to remove many to many dependency

* feat: implemented list manipulation instead of for loop

* feat: added readonly functionality in `read-only` editor core

* feat: added UI Changes for read-only mode

* feat: added mentions store in web Root Store

* chore: renamed `use-editor-suggestions` hook

* feat: UI Improvements for conditional highlights w.r.t readonly in mentionNode

* fix: removed mentions from `filter_set` parameters

* fix: minor merge fixes

* fix: package lock updates

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-11-01 16:36:37 +05:30
Aaryan Khandelwal
490e032ac6 style: new avatar and avatar group components (#2584)
* style: new avatar components

* chore: bug fixes

* chore: add pixel to size

* chore: add comments to helper functions

* fix: build errors
2023-11-01 15:24:11 +05:30
Bavisetti Narayan
1a24f9ec25 chore: removed start date and target date filter (#2582) 2023-11-01 14:34:03 +05:30
Bavisetti Narayan
2cb94b4105 chore: added permission for importer (#2577) 2023-11-01 14:32:33 +05:30
Bavisetti Narayan
ecde7edf09 fix: removed numbers from magic code (#2570) 2023-11-01 14:31:42 +05:30
Bavisetti Narayan
02f4916e49 chore: workspace members and project members endpoint (#2560)
* fix: removed members endpoint

* fix: changed project permisson class for project member

* fix: permission changed in workspace and project

* fix: added project filter in members
2023-11-01 14:30:45 +05:30
Anmol Singh Bhatia
1be82814fc style: issue peek overview ui improvement (#2574)
* style: issue peek overview ui improvement

* chore: implemented issue subscription in peek overview

* chore: issue properties dropdown refactor

* fix: build error

* chore: label select refactor

* chore: issue peekoverview revamp and refactor

* chore: issue peekoverview properties added and code refactor

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-11-01 14:22:29 +05:30
sriram veeraghanta
10e35d9a06 chore: delete unused files (#2585) 2023-11-01 13:45:04 +05:30
Dakshesh Jain
2d64caef90 refactor: project settings (#2575)
* refactor: project setting estimate

* refactor: project setting label

* refactor: project setting state

* refactor: project setting integration

* refactor: project settings member

* fix: estimate not updating

* fix: estimate not in observable

* fix: build error
2023-11-01 13:42:51 +05:30
deepsource-autofix[bot]
80e6d7e1ea refactor: remove true from boolean attribute (#2579)
When using a boolean attribute in JSX, you can set the attribute value to true or omit the value. This helps to keep consistency in code.

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2023-11-01 12:30:21 +05:30
sriram veeraghanta
b7d5a42d45 fix: added deepsource config file (#2578) 2023-10-31 19:52:13 +05:30
sriram veeraghanta
2b1e1557ca fix: upgrading to turbo new version (#2576) 2023-10-31 19:27:56 +05:30
Aaryan Khandelwal
705b33377c fix: members list endpoint authorization (#2571)
* fix: members list endpoint authorization

* chore: update user types
2023-10-31 13:11:13 +05:30
Nikhil
49fd4427c8 chore: user settings endpoint (#2557)
* chore: user settings endpoint

* dev: fix the user settings
2023-10-31 13:08:13 +05:30
Bavisetti Narayan
bdbb64f385 fix: changed assignees and labels in pages and modules (#2553) 2023-10-31 13:06:57 +05:30
Aaryan Khandelwal
98716859d5 chore: reove unused files (#2567) 2023-10-31 12:43:08 +05:30
M. Palanikannan
8072bbb559 fix: Debounce title and Editor initialization (#2530)
* fixed debounce logic and extracted the same

* fixed editor mounting with custom hook

* removed console logs and improved structure

* fixed comment editor behavior on Shift-Enter

* fixed editor initialization behaviour for new peek view

* fixed button type to avoid reload while editing comments

* fixed initialization of content in peek overview

* improved naming variables in updated title debounce logic

* added react-hook-form support to the issue detail in peek view with save states

* delete image plugin's ts support improved
2023-10-31 12:26:10 +05:30
Aaryan Khandelwal
442c83eea2 style: spreadsheet columns (#2554)
* style: spreadsheet columns

* fix: build errors
2023-10-31 12:18:04 +05:30
Aaryan Khandelwal
cb533849e8 chore: update members endpoint (#2569) 2023-10-31 12:16:40 +05:30
Aaryan Khandelwal
59c52023fb style: list layout (#2566) 2023-10-31 12:14:06 +05:30
Aaryan Khandelwal
08ca016f65 fix: custom theme form validations (#2565) 2023-10-31 12:12:24 +05:30
Aaryan Khandelwal
1c2ea6da5e fix: edit project button redirection (#2564)
* fix: redirect to project settings

* fix: 404 page button alignment
2023-10-31 12:06:55 +05:30
Aaryan Khandelwal
8b7b5c54b9 fix: global views bugs (#2563) 2023-10-31 12:06:11 +05:30
guru_sainath
52474715de chore: handled next_url redirection issue (#2562) 2023-10-31 12:04:36 +05:30
Aaryan Khandelwal
dcf81e28e4 dev: implemented MobX in workspace settings and create workspace form (#2561)
* dev: implement mobx store for workspace settings

* chore: workspace general settings mobx integration

* chore: workspace members settings mobx integration
2023-10-30 20:38:50 +05:30
Aaryan Khandelwal
050406b8a4 chore: add empty state for list and spreadsheet layouts (#2531)
* chore: add empty state for list and spreadsheet layouts

* fix: build errors
2023-10-30 20:09:04 +05:30
Anmol Singh Bhatia
8eaac60aa5 style: cycle ui revamp, and chore: code refactor (#2558)
* chore: cycle custom svg icon added and code refactor

* chore: module code refactor

* style: cycle ui revamp and code refactor

* chore: cycle card view layout fix

* chore: layout fix

* style: module and cycle title tooltip position
2023-10-30 19:22:27 +05:30
Nikhil
7edaa49c21 revert: issues endpoint (#2555) 2023-10-30 15:05:25 +05:30
Dakshesh Jain
8cc61bc427 fix: html sensitization function (#2552) 2023-10-30 13:59:00 +05:30
Anmol Singh Bhatia
fc82d6fc23 style: module ui revamp (#2548)
* chore: module constant and helper function added

* style: module card ui revamp

* chore: custom media query added

* chore: circular progress indicator added

* chore: module card item ui improvement

* chore: module list view added

* chore: module sidebar added in list and card view

* chore: module list and card ui improvement

* chore: module sidebar select, avatar and link list component improvement

* chore: sidebar improvement and refactor

* style: module sidebar revamp

* style: module sidebar ui improvement

* chore: module sidebar lead and member select improvement

* style: module sidebar progress section empty state added

* chore: module card issue count validation added

* style: module card and list item ui improvement
2023-10-27 18:45:10 +05:30
Bavisetti Narayan
080b5a29ae refactor: issue activity (#2503)
* dev: update project and workspace save in issue activity

* chore: issue activity structuring

* chore: added workspace id

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-10-27 15:38:54 +05:30
Bavisetti Narayan
9ee3fb9c6c chore: removed duplicate issues (#2418) 2023-10-27 15:37:26 +05:30
Bavisetti Narayan
b0a24ab57b fix: issue filters validation (#2417) 2023-10-27 15:36:27 +05:30
Nikhil
3e706f9653 chore: project create/update endpoint to be simillar to list (#2476)
* chore: project create endpoint to be simillar to list

* dev: make project create and update response same
2023-10-27 15:35:15 +05:30
Nikhil
4e86110123 chore: database configuration (#2497) 2023-10-27 15:34:01 +05:30
Nikhil
6bebb8a93b chore: user issue display properties (#2258)
* chore: user issue display properties

* chore: added issue property

* fix: migrations and url change

* dev: add a default condition on get for issue properties

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-10-27 15:32:42 +05:30
Nikhil
c8f98a9bc2 chore: make module create and list endpoint response structure simillar (#2524) 2023-10-27 15:31:15 +05:30
Nikhil
55b2927a17 chore: total issues count for issue listing endpoint (#2534)
* chore: total issues count for issue listing endpoint

* dev: add print for DEBUG mode

* fix: changed assignees_list and label_list

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-10-27 15:30:28 +05:30
sriram veeraghanta
597ea26d7b fix: enabling the nextjs static site generation for web and space (#2547) 2023-10-27 13:14:03 +05:30
Dakshesh Jain
4aad35e007 refactor: quick add (#2541)
* refactor: store and helper setup for quick-add

* refactor: kanban quick add with optimistic issue create

* refactor: added function definition

* refactor: list quick add with optimistic issue create

* refactor: spreadsheet quick add with optimistic issue create

* refactor: calender quick add with optimistic issue create

* refactor: gantt quick add with optimistic issue create

* refactor: input component and pre-loading data logic

* style: calender quick-add height and content shift

* feat: sub-group quick-add issue

* feat: displaying loading state when issue is being created

* fix: setting string null to null
2023-10-27 12:32:24 +05:30
Aaryan Khandelwal
d95ea463b2 chore: implement mobx in project features settings (#2533) 2023-10-26 14:58:00 +05:30
Henit Chobisa
993b388f00 chore: added missing dependencies in packages (#2540) 2023-10-26 12:37:24 +05:30
Aaryan Khandelwal
a49f00bd39 chore: refactor and beautify issue properties (#2539)
* chore: update all issue property components

* style: issue properties
2023-10-25 19:47:58 +05:30
guru_sainath
ca2da41dd2 chore: issue comment reaction workflow and mutation update (#2537) 2023-10-25 19:24:14 +05:30
guru_sainath
a6d741e784 chore: implemented drag and drop between dates for project issues, cycle, module, and project views for calendar layout (#2535) 2023-10-25 16:09:50 +05:30
Anmol Singh Bhatia
cea39c758e chore: layout refactor (#2532)
* chore: layout refactor

* fix: profile auth issue

* chore: project setting layout refactor

* chore: workspace layout refactor

* chore: profile layout refactor

* chore: layout import refactor
2023-10-25 15:48:57 +05:30
Aaryan Khandelwal
d72d3da6de refactor: modules list page (#2521)
* refactor: modules list page

* chore: update handle favorites logic for modules

* fix: build errors
2023-10-23 19:17:42 +05:30
sriram veeraghanta
07d548ea43 fix: cycle modal redendent component fix (#2528) 2023-10-23 18:38:01 +05:30
Anmol Singh Bhatia
08f7ac6da7 chore: sidebar fix and ui improvement (#2527)
* fix: cycle sidebar overlapping fix

* style: issue sidebar related to and duplicate icon fix
2023-10-23 18:07:38 +05:30
Anmol Singh Bhatia
fcf9851ee4 style: command modal style (#2526) 2023-10-23 17:40:59 +05:30
Aaryan Khandelwal
f6c1dad342 fix: extend custom tailwind valiables instead of overwriting (#2525) 2023-10-23 17:40:17 +05:30
Anmol Singh Bhatia
4c54d826ba fix: command palette fix (#2523) 2023-10-23 17:24:43 +05:30
Anmol Singh Bhatia
1786a395dc chore: layout refactor (#2522)
* chore: pages layout refactor

* chore: view layout refactor

* chore: view layout refactor

* chore: inbox layout refactor

* chore: draft issue layout refactor

* chore: archived issue layout refactor

* chore: draft issue header layout fix

* chore: layout code refactor

* chore: code refactor

* chore: project setting layout fix
2023-10-23 16:54:26 +05:30
Manish Gupta
d7a36f5b04 dev: Self Hosting simplified with shell script (#2484)
* bug:fix recent page hiding last item on scroll #1468 (#2411)

* dev: hub compose file update (#2376) (#2444) (#2445)

* docker-compose-hub modified for envs

* bug:fix recent page hiding last item on scroll #1468 (#2411)

* wip

* fixed the AMD build on ARM

---------

Co-authored-by: Manish Gupta <59428681+manishg3@users.noreply.github.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>

* selfhosting fixes

* wip

* checking selfhost

* checking selfhost

* selfhost

* selfhosting script fix

* wip

* self hosting fixes

* folder structure modified

* replica config

* self host specific version

* fix

* fix

* fixes

* docker compose modifications

* fixed install.sh

* fixed install.sh

* install.sh modified

---------

Co-authored-by: Prashant Indurkar <32466796+PrashantIndurkar@users.noreply.github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-10-23 15:56:19 +05:30
Aaryan Khandelwal
edc5a38973 chore: removed material icons package (#2518) 2023-10-23 15:07:09 +05:30
Aaryan Khandelwal
38421e8106 refactor: layout roots (#2517) 2023-10-23 15:06:28 +05:30
Aaryan Khandelwal
05a76c5ee3 fix: gantt chart blocks drag and resize logic (#2516) 2023-10-23 15:06:02 +05:30
Aaryan Khandelwal
c739b7235d refactor: publish project modal (#2514)
* chore: add publish badge to the header

* refactor: project oublish components

* chore: remove link tag
2023-10-23 12:12:42 +05:30
802 changed files with 26615 additions and 30477 deletions

17
.deepsource.toml Normal file
View File

@@ -0,0 +1,17 @@
version = 1
[[analyzers]]
name = "shell"
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"

213
.github/workflows/build-branch.yml vendored Normal file
View File

@@ -0,0 +1,213 @@
name: Branch Build
on:
pull_request:
types:
- closed
branches:
- master
- release
- qa
- develop
env:
TARGET_BRANCH: ${{ github.event.pull_request.base.ref }}
jobs:
branch_build_and_push:
if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) }}
name: Build-Push Web/Space/API/Proxy Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
# - name: Set Target Branch Name on PR close
# if: ${{ github.event_name == 'pull_request' && github.event.action =='closed' }}
# run: echo "TARGET_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV
# - name: Set Target Branch Name on other than PR close
# if: ${{ github.event_name == 'push' }}
# run: echo "TARGET_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
- uses: ASzc/change-string-case-action@v2
id: gh_branch_upper_lower
with:
string: ${{env.TARGET_BRANCH}}
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_replace_slash
with:
source: ${{ steps.gh_branch_upper_lower.outputs.lowercase }}
find: '/'
replace: '-'
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_replace_dot
with:
source: ${{ steps.gh_branch_replace_slash.outputs.value }}
find: '.'
replace: ''
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_clean
with:
source: ${{ steps.gh_branch_replace_dot.outputs.value }}
find: '_'
replace: ''
- name: Uploading Proxy Source
uses: actions/upload-artifact@v3
with:
name: proxy-src-code
path: ./nginx
- name: Uploading Backend Source
uses: actions/upload-artifact@v3
with:
name: backend-src-code
path: ./apiserver
- name: Uploading Web Source
uses: actions/upload-artifact@v3
with:
name: web-src-code
path: |
./
!./apiserver
!./nginx
!./deploy
!./space
- name: Uploading Space Source
uses: actions/upload-artifact@v3
with:
name: space-src-code
path: |
./
!./apiserver
!./nginx
!./deploy
!./web
outputs:
gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }}
branch_build_push_frontend:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Web Source Code
uses: actions/download-artifact@v3
with:
name: web-src-code
- name: Build and Push Frontend to Docker Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./web/Dockerfile.web
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_space:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Space Source Code
uses: actions/download-artifact@v3
with:
name: space-src-code
- name: Build and Push Space to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./space/Dockerfile.space
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_backend:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Backend Source Code
uses: actions/download-artifact@v3
with:
name: backend-src-code
- name: Build and Push Backend to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./Dockerfile.api
platforms: linux/amd64
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_proxy:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Downloading Proxy Source Code
uses: actions/download-artifact@v3
with:
name: proxy-src-code
- name: Build and Push Plane-Proxy to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}

7
.gitignore vendored
View File

@@ -16,7 +16,8 @@ node_modules
# Production
/build
dist
dist/
out/
# Misc
.DS_Store
@@ -74,7 +75,7 @@ pnpm-lock.yaml
pnpm-workspace.yaml
.npmrc
.secrets
tmp/
## packages
dist

View File

@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
hello@plane.so.
squawk@plane.so.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

View File

@@ -7,7 +7,7 @@
</p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source, self-hosted project planning tool</b></p>
<p align="center"><b>Flexible, extensible open-source project management</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">

View File

@@ -1,4 +1,4 @@
import os, sys, random, string
import os, sys
import uuid
sys.path.append("/code")

View File

@@ -3,4 +3,4 @@ from psycogreen.gevent import patch_psycopg
def post_fork(server, worker):
patch_psycopg()
worker.log.info("Made Psycopg2 Green")
worker.log.info("Made Psycopg2 Green")

View File

@@ -101,4 +101,4 @@ class ProjectLitePermission(BasePermission):
workspace__slug=view.workspace_slug,
member=request.user,
project_id=view.project_id,
).exists()
).exists()

View File

@@ -17,7 +17,7 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
return AnalyticView.objects.create(**validated_data)
def update(self, instance, validated_data):
@@ -25,6 +25,6 @@ class AnalyticViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)

View File

@@ -1,6 +1,3 @@
# Django imports
from django.db.models.functions import TruncDate
# Third party imports
from rest_framework import serializers

View File

@@ -6,7 +6,6 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .project import ProjectLiteSerializer
from .user import UserLiteSerializer
from plane.db.models import Inbox, InboxIssue, Issue

View File

@@ -5,4 +5,4 @@ from .github import (
GithubIssueSyncSerializer,
GithubCommentSyncSerializer,
)
from .slack import SlackProjectSyncSerializer
from .slack import SlackProjectSyncSerializer

View File

@@ -8,8 +8,7 @@ from rest_framework import serializers
from .base import BaseSerializer
from .user import UserLiteSerializer
from .state import StateSerializer, StateLiteSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import (
User,
@@ -75,13 +74,13 @@ class IssueCreateSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
assignees_list = serializers.ListField(
assignees = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
labels_list = serializers.ListField(
labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
@@ -99,6 +98,12 @@ class IssueCreateSerializer(BaseSerializer):
"updated_at",
]
def to_representation(self, instance):
data = super().to_representation(instance)
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
data['labels'] = [str(label.id) for label in instance.labels.all()]
return data
def validate(self, data):
if (
data.get("start_date", None) is not None
@@ -109,8 +114,8 @@ class IssueCreateSerializer(BaseSerializer):
return data
def create(self, validated_data):
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
@@ -168,8 +173,8 @@ class IssueCreateSerializer(BaseSerializer):
return issue
def update(self, instance, validated_data):
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("labels", None)
# Related models
project_id = instance.project_id
@@ -226,25 +231,6 @@ class IssueActivitySerializer(BaseSerializer):
fields = "__all__"
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueComment
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssuePropertySerializer(BaseSerializer):
class Meta:
@@ -281,7 +267,6 @@ class LabelLiteSerializer(BaseSerializer):
class IssueLabelSerializer(BaseSerializer):
# label_details = LabelSerializer(read_only=True, source="label")
class Meta:
model = IssueLabel

View File

@@ -4,9 +4,8 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .issue import IssueStateSerializer
from plane.db.models import (
User,
@@ -19,7 +18,7 @@ from plane.db.models import (
class ModuleWriteSerializer(BaseSerializer):
members_list = serializers.ListField(
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
@@ -39,6 +38,11 @@ class ModuleWriteSerializer(BaseSerializer):
"created_at",
"updated_at",
]
def to_representation(self, instance):
data = super().to_representation(instance)
data['members'] = [str(member.id) for member in instance.members.all()]
return data
def validate(self, data):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
@@ -46,7 +50,7 @@ class ModuleWriteSerializer(BaseSerializer):
return data
def create(self, validated_data):
members = validated_data.pop("members_list", None)
members = validated_data.pop("members", None)
project = self.context["project"]
@@ -72,7 +76,7 @@ class ModuleWriteSerializer(BaseSerializer):
return module
def update(self, instance, validated_data):
members = validated_data.pop("members_list", None)
members = validated_data.pop("members", None)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()

View File

@@ -33,7 +33,7 @@ class PageBlockLiteSerializer(BaseSerializer):
class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
labels_list = serializers.ListField(
labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
@@ -50,9 +50,13 @@ class PageSerializer(BaseSerializer):
"project",
"owned_by",
]
def to_representation(self, instance):
data = super().to_representation(instance)
data['labels'] = [str(label.id) for label in instance.labels.all()]
return data
def create(self, validated_data):
labels = validated_data.pop("labels_list", None)
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
page = Page.objects.create(
@@ -77,7 +81,7 @@ class PageSerializer(BaseSerializer):
return page
def update(self, instance, validated_data):
labels = validated_data.pop("labels_list", None)
labels = validated_data.pop("labels", None)
if labels is not None:
PageLabel.objects.filter(page=instance).delete()
PageLabel.objects.bulk_create(

View File

@@ -79,14 +79,14 @@ class UserMeSettingsSerializer(BaseSerializer):
email=obj.email
).count()
if obj.last_workspace_id is not None:
workspace = Workspace.objects.get(
workspace = Workspace.objects.filter(
pk=obj.last_workspace_id, workspace_member__member=obj.id
)
).first()
return {
"last_workspace_id": obj.last_workspace_id,
"last_workspace_slug": workspace.slug,
"last_workspace_slug": workspace.slug if workspace is not None else "",
"fallback_workspace_id": obj.last_workspace_id,
"fallback_workspace_slug": workspace.slug,
"fallback_workspace_slug": workspace.slug if workspace is not None else "",
"invites": workspace_invites,
}
else:

View File

@@ -57,7 +57,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data):
@@ -65,7 +65,7 @@ class IssueViewSerializer(BaseSerializer):
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = {}
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)

View File

@@ -110,9 +110,8 @@ class TeamSerializer(BaseSerializer):
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return team
else:
team = Team.objects.create(**validated_data)
return team
team = Team.objects.create(**validated_data)
return team
def update(self, instance, validated_data):
if "members" in validated_data:
@@ -124,8 +123,7 @@ class TeamSerializer(BaseSerializer):
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data)
else:
return super().update(instance, validated_data)
return super().update(instance, validated_data)
class WorkspaceThemeSerializer(BaseSerializer):

View File

@@ -1,7 +1,7 @@
from .analytic import urlpatterns as analytic_urls
from .asset import urlpatterns as asset_urls
from .authentication import urlpatterns as authentication_urls
from .configuration import urlpatterns as configuration_urls
from .config import urlpatterns as configuration_urls
from .cycle import urlpatterns as cycle_urls
from .estimate import urlpatterns as estimate_urls
from .gpt import urlpatterns as gpt_urls

View File

@@ -17,7 +17,7 @@ from plane.api.views import (
IssueSubscriberViewSet,
IssueReactionViewSet,
CommentReactionViewSet,
IssuePropertyViewSet,
IssueUserDisplayPropertyEndpoint,
IssueArchiveViewSet,
IssueRelationViewSet,
IssueDraftViewSet,
@@ -235,28 +235,11 @@ urlpatterns = [
## End Comment Reactions
## IssueProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
IssuePropertyViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-roadmap",
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
IssuePropertyViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-roadmap",
),
## IssueProperty Ebd
## IssueProperty End
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",

View File

@@ -4,17 +4,15 @@ from plane.api.views import (
ProjectViewSet,
InviteProjectEndpoint,
ProjectMemberViewSet,
ProjectMemberEndpoint,
ProjectMemberInvitationsViewset,
ProjectMemberUserEndpoint,
AddMemberToProjectEndpoint,
ProjectJoinEndpoint,
AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint
ProjectPublicCoverImagesEndpoint,
)
@@ -53,7 +51,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
ProjectMemberViewSet.as_view({"get": "list"}),
ProjectMemberViewSet.as_view({"get": "list", "post": "create"}),
name="project-member",
),
path(
@@ -67,16 +65,6 @@ urlpatterns = [
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
ProjectMemberEndpoint.as_view(),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/add/",
AddMemberToProjectEndpoint.as_view(),
name="project",
),
path(
"workspaces/<str:slug>/projects/join/",
ProjectJoinEndpoint.as_view(),

View File

@@ -5,7 +5,6 @@ from plane.api.views import (
WorkSpaceViewSet,
InviteWorkspaceEndpoint,
WorkSpaceMemberViewSet,
WorkspaceMembersEndpoint,
WorkspaceInvitationsViewset,
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
@@ -86,11 +85,6 @@ urlpatterns = [
),
name="workspace-member",
),
path(
"workspaces/<str:slug>/workspace-members/",
WorkspaceMembersEndpoint.as_view(),
name="workspace-members",
),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view(

View File

@@ -28,7 +28,6 @@ from plane.api.views import (
## End User
# Workspaces
WorkSpaceViewSet,
UserWorkspaceInvitationsEndpoint,
UserWorkSpacesEndpoint,
InviteWorkspaceEndpoint,
JoinWorkspaceEndpoint,
@@ -82,7 +81,7 @@ from plane.api.views import (
BulkDeleteIssuesEndpoint,
BulkImportIssuesEndpoint,
ProjectUserViewsEndpoint,
IssuePropertyViewSet,
IssueUserDisplayPropertyEndpoint,
LabelViewSet,
SubIssuesEndpoint,
IssueLinkViewSet,
@@ -1008,26 +1007,9 @@ urlpatterns = [
## End Comment Reactions
## IssueProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
IssuePropertyViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-roadmap",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/<uuid:pk>/",
IssuePropertyViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-roadmap",
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
## IssueProperty Ebd
## Issue Archives

View File

@@ -7,14 +7,12 @@ from .project import (
ProjectMemberInvitationsViewset,
ProjectMemberInviteDetailViewSet,
ProjectIdentifierEndpoint,
AddMemberToProjectEndpoint,
ProjectJoinEndpoint,
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectMemberEndpoint,
WorkspaceProjectDeployBoardEndpoint,
LeaveProjectEndpoint,
ProjectPublicCoverImagesEndpoint,
@@ -53,7 +51,6 @@ from .workspace import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceMembersEndpoint,
LeaveWorkspaceEndpoint,
)
from .state import StateViewSet
@@ -71,7 +68,7 @@ from .issue import (
WorkSpaceIssuesEndpoint,
IssueActivityEndpoint,
IssueCommentViewSet,
IssuePropertyViewSet,
IssueUserDisplayPropertyEndpoint,
LabelViewSet,
BulkDeleteIssuesEndpoint,
UserWorkSpaceIssues,
@@ -169,4 +166,4 @@ from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkA
from .exporter import ExportIssuesEndpoint
from .config import ConfigurationEndpoint
from .config import ConfigurationEndpoint

View File

@@ -55,11 +55,11 @@ class VerifyEmailEndpoint(BaseAPIView):
return Response(
{"email": "Successfully activated"}, status=status.HTTP_200_OK
)
except jwt.ExpiredSignatureError as indentifier:
except jwt.ExpiredSignatureError as _indentifier:
return Response(
{"email": "Activation expired"}, status=status.HTTP_400_BAD_REQUEST
)
except jwt.exceptions.DecodeError as indentifier:
except jwt.exceptions.DecodeError as _indentifier:
return Response(
{"email": "Invalid token"}, status=status.HTTP_400_BAD_REQUEST
)

View File

@@ -249,11 +249,11 @@ class MagicSignInGenerateEndpoint(BaseAPIView):
## Generate a random token
token = (
"".join(random.choices(string.ascii_lowercase + string.digits, k=4))
"".join(random.choices(string.ascii_lowercase, k=4))
+ "-"
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
+ "".join(random.choices(string.ascii_lowercase, k=4))
+ "-"
+ "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
+ "".join(random.choices(string.ascii_lowercase, k=4))
)
ri = redis_instance()

View File

@@ -84,6 +84,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
capture_exception(e)
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
print(e) if settings.DEBUG else print("Server Error")
capture_exception(e)
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@@ -161,6 +162,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
if isinstance(e, KeyError):
return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST)
print(e) if settings.DEBUG else print("Server Error")
capture_exception(e)
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@@ -21,8 +21,8 @@ class ConfigurationEndpoint(BaseAPIView):
def get(self, request):
data = {}
data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None)
data["github"] = os.environ.get("GITHUB_CLIENT_ID", None)
data["google_client_id"] = os.environ.get("GOOGLE_CLIENT_ID", None)
data["github_client_id"] = os.environ.get("GITHUB_CLIENT_ID", None)
data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None)
data["magic_login"] = (
bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD)
@@ -30,4 +30,5 @@ class ConfigurationEndpoint(BaseAPIView):
data["email_password_login"] = (
os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
)
data["slack_client_id"] = os.environ.get("SLACK_CLIENT_ID", None)
return Response(data, status=status.HTTP_200_OK)

View File

@@ -3,7 +3,6 @@ import json
# Django imports
from django.db.models import (
OuterRef,
Func,
F,
Q,
@@ -588,14 +587,14 @@ class CycleIssueViewSet(BaseViewSet):
)
if group_by:
grouped_results = group_results(issues_data, group_by, sub_group_by)
return Response(
group_results(issues_data, group_by, sub_group_by),
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
issues_data,
status=status.HTTP_200_OK,
issues_data, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id, cycle_id):

View File

@@ -39,6 +39,7 @@ from plane.utils.integrations.github import get_github_repo_details
from plane.utils.importers.jira import jira_project_issue_summary
from plane.bgtasks.importer_task import service_importer
from plane.utils.html_processor import strip_tags
from plane.api.permissions import WorkSpaceAdminPermission
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
@@ -119,6 +120,9 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
class ImportServiceEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def post(self, request, slug, service):
project_id = request.data.get("project_id", False)

View File

@@ -360,8 +360,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
)
.select_related("issue", "workspace", "project")
)
else:
return InboxIssue.objects.none()
return InboxIssue.objects.none()
def list(self, request, slug, project_id, inbox_id):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)

View File

@@ -1,6 +1,6 @@
# Python improts
import uuid
import requests
# Django imports
from django.contrib.auth.hashers import make_password
@@ -25,7 +25,7 @@ from plane.utils.integrations.github import (
delete_github_installation,
)
from plane.api.permissions import WorkSpaceAdminPermission
from plane.utils.integrations.slack import slack_oauth
class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer
@@ -98,12 +98,19 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
config = {"installation_id": installation_id}
if provider == "slack":
metadata = request.data.get("metadata", {})
code = request.data.get("code", False)
if not code:
return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST)
slack_response = slack_oauth(code=code)
metadata = slack_response
access_token = metadata.get("access_token", False)
team_id = metadata.get("team", {}).get("id", False)
if not metadata or not access_token or not team_id:
return Response(
{"error": "Access token and team id is required"},
{"error": "Slack could not be installed. Please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
config = {"team_id": team_id, "access_token": access_token}

View File

@@ -11,6 +11,7 @@ from plane.api.views import BaseViewSet, BaseAPIView
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
from plane.api.serializers import SlackProjectSyncSerializer
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
from plane.utils.integrations.slack import slack_oauth
class SlackProjectSyncViewSet(BaseViewSet):
@@ -32,25 +33,47 @@ class SlackProjectSyncViewSet(BaseViewSet):
)
def create(self, request, slug, project_id, workspace_integration_id):
serializer = SlackProjectSyncSerializer(data=request.data)
try:
code = request.data.get("code", False)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
if not code:
return Response(
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
)
if serializer.is_valid():
serializer.save(
project_id=project_id,
workspace_integration_id=workspace_integration_id,
slack_response = slack_oauth(code=code)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
workspace_integration = WorkspaceIntegration.objects.get(
pk=workspace_integration_id, workspace__slug=slug
)
slack_project_sync = SlackProjectSync.objects.create(
access_token=slack_response.get("access_token"),
scopes=slack_response.get("scope"),
bot_user_id=slack_response.get("bot_user_id"),
webhook_url=slack_response.get("incoming_webhook", {}).get("url"),
data=slack_response,
team_id=slack_response.get("team", {}).get("id"),
team_name=slack_response.get("team", {}).get("name"),
workspace_integration=workspace_integration,
project_id=project_id,
)
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor, role=20, project_id=project_id
)
serializer = SlackProjectSyncSerializer(slack_project_sync)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "Slack is already installed for the project"},
status=status.HTTP_410_GONE,
)
capture_exception(e)
return Response(
{"error": "Slack could not be installed. Please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@@ -39,7 +39,6 @@ from plane.api.serializers import (
IssueActivitySerializer,
IssueCommentSerializer,
IssuePropertySerializer,
LabelSerializer,
IssueSerializer,
LabelSerializer,
IssueFlatSerializer,
@@ -130,7 +129,7 @@ class IssueViewSet(BaseViewSet):
queryset=IssueReaction.objects.select_related("actor"),
)
)
)
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
@@ -229,8 +228,9 @@ class IssueViewSet(BaseViewSet):
)
if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response(
group_results(issues, group_by, sub_group_by),
grouped_results,
status=status.HTTP_200_OK,
)
@@ -433,8 +433,9 @@ class UserWorkSpaceIssues(BaseAPIView):
)
if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response(
group_results(issues, group_by, sub_group_by),
grouped_results,
status=status.HTTP_200_OK,
)
@@ -597,41 +598,12 @@ class IssueCommentViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class IssuePropertyViewSet(BaseViewSet):
serializer_class = IssuePropertySerializer
model = IssueProperty
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
ProjectLitePermission,
]
filterset_fields = []
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"), user=self.request.user
)
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(user=self.request.user)
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
serializer = IssuePropertySerializer(queryset, many=True)
return Response(
serializer.data[0] if len(serializer.data) > 0 else [],
status=status.HTTP_200_OK,
)
def create(self, request, slug, project_id):
def post(self, request, slug, project_id):
issue_property, created = IssueProperty.objects.get_or_create(
user=request.user,
project_id=project_id,
@@ -640,15 +612,18 @@ class IssuePropertyViewSet(BaseViewSet):
if not created:
issue_property.properties = request.data.get("properties", {})
issue_property.save()
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)
issue_property.properties = request.data.get("properties", {})
issue_property.save()
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug, project_id):
issue_property, _ = IssueProperty.objects.get_or_create(
user=request.user, project_id=project_id
)
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)
class LabelViewSet(BaseViewSet):
serializer_class = LabelSerializer
@@ -798,6 +773,20 @@ class SubIssuesEndpoint(BaseAPIView):
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
# Track the issue
_ = [
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"parent": str(issue_id)}),
actor_id=str(request.user.id),
issue_id=str(sub_issue_id),
project_id=str(project_id),
current_instance=json.dumps({"parent": str(sub_issue_id)}),
epoch=int(timezone.now().timestamp()),
)
for sub_issue_id in sub_issue_ids
]
return Response(
IssueFlatSerializer(updated_sub_issues, many=True).data,
status=status.HTTP_200_OK,
@@ -963,8 +952,8 @@ class IssueAttachmentEndpoint(BaseAPIView):
issue_attachments = IssueAttachment.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
)
serilaizer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serilaizer.data, status=status.HTTP_200_OK)
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class IssueArchiveViewSet(BaseViewSet):
@@ -1110,17 +1099,19 @@ class IssueArchiveViewSet(BaseViewSet):
archived_at__isnull=False,
pk=pk,
)
issue.archived_at = None
issue.save()
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": None}),
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
current_instance=json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder
),
epoch=int(timezone.now().timestamp()),
)
issue.archived_at = None
issue.save()
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
@@ -1165,9 +1156,7 @@ class IssueSubscriberViewSet(BaseViewSet):
def list(self, request, slug, project_id, issue_id):
members = (
ProjectMember.objects.filter(
workspace__slug=slug, project_id=project_id
)
ProjectMember.objects.filter(workspace__slug=slug, project_id=project_id)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
@@ -1416,8 +1405,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
)
.distinct()
).order_by("created_at")
else:
return IssueComment.objects.none()
return IssueComment.objects.none()
except ProjectDeployBoard.DoesNotExist:
return IssueComment.objects.none()
@@ -1542,8 +1530,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
.order_by("-created_at")
.distinct()
)
else:
return IssueReaction.objects.none()
return IssueReaction.objects.none()
except ProjectDeployBoard.DoesNotExist:
return IssueReaction.objects.none()
@@ -1638,8 +1625,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
.order_by("-created_at")
.distinct()
)
else:
return CommentReaction.objects.none()
return CommentReaction.objects.none()
except ProjectDeployBoard.DoesNotExist:
return CommentReaction.objects.none()
@@ -1733,8 +1719,7 @@ class IssueVotePublicViewSet(BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
)
else:
return IssueVote.objects.none()
return IssueVote.objects.none()
except ProjectDeployBoard.DoesNotExist:
return IssueVote.objects.none()
@@ -2174,7 +2159,11 @@ class IssueDraftViewSet(BaseViewSet):
## Grouping the results
group_by = request.GET.get("group_by", False)
if group_by:
return Response(group_results(issues, group_by), status=status.HTTP_200_OK)
grouped_results = group_results(issues, group_by)
return Response(
grouped_results,
status=status.HTTP_200_OK,
)
return Response(issues, status=status.HTTP_200_OK)

View File

@@ -149,6 +149,9 @@ class ModuleViewSet(BaseViewSet):
if serializer.is_valid():
serializer.save()
module = Module.objects.get(pk=serializer.data["id"])
serializer = ModuleSerializer(module)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -361,7 +364,6 @@ class ModuleIssueViewSet(BaseViewSet):
.values("count")
)
)
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
@@ -371,14 +373,14 @@ class ModuleIssueViewSet(BaseViewSet):
)
if group_by:
grouped_results = group_results(issues_data, group_by, sub_group_by)
return Response(
group_results(issues_data, group_by, sub_group_by),
grouped_results,
status=status.HTTP_200_OK,
)
return Response(
issues_data,
status=status.HTTP_200_OK,
issues_data, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id, module_id):

View File

@@ -11,7 +11,6 @@ from django.conf import settings
from rest_framework.response import Response
from rest_framework import exceptions
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework import status
from sentry_sdk import capture_exception
@@ -113,7 +112,7 @@ def get_user_data(access_token: str) -> dict:
url="https://api.github.com/user/emails", headers=headers
).json()
[
_ = [
user_data.update({"email": item.get("email")})
for item in response
if item.get("primary") is True
@@ -147,7 +146,7 @@ class OauthEndpoint(BaseAPIView):
data = get_user_data(access_token)
email = data.get("email", None)
if email == None:
if email is None:
return Response(
{
"error": "Something went wrong. Please try again later or contact the support team."
@@ -158,7 +157,6 @@ class OauthEndpoint(BaseAPIView):
if "@" in email:
user = User.objects.get(email=email)
email = data["email"]
channel = "email"
mobile_number = uuid.uuid4().hex
email_verified = True
else:
@@ -182,7 +180,7 @@ class OauthEndpoint(BaseAPIView):
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = request.META.get("REMOTE_ADDR")
user.last_login_medium = f"oauth"
user.last_login_medium = "oauth"
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.is_email_verified = email_verified
user.save()
@@ -233,7 +231,6 @@ class OauthEndpoint(BaseAPIView):
if "@" in email:
email = data["email"]
mobile_number = uuid.uuid4().hex
channel = "email"
email_verified = True
else:
return Response(

View File

@@ -1,5 +1,5 @@
# Python imports
from datetime import timedelta, datetime, date
from datetime import timedelta, date
# Django imports
from django.db.models import Exists, OuterRef, Q, Prefetch

View File

@@ -11,7 +11,6 @@ from django.db.models import (
Q,
Exists,
OuterRef,
Func,
F,
Func,
Subquery,
@@ -35,7 +34,6 @@ from plane.api.serializers import (
ProjectDetailSerializer,
ProjectMemberInviteSerializer,
ProjectFavoriteSerializer,
IssueLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
)
@@ -69,6 +67,7 @@ from plane.db.models import (
ModuleMember,
Inbox,
ProjectDeployBoard,
IssueProperty,
)
from plane.bgtasks.project_invitation_task import project_invitation
@@ -83,7 +82,7 @@ class ProjectViewSet(BaseViewSet):
]
def get_serializer_class(self, *args, **kwargs):
if self.action == "update" or self.action == "partial_update":
if self.action in ["update", "partial_update"]:
return ProjectSerializer
return ProjectDetailSerializer
@@ -201,6 +200,11 @@ class ProjectViewSet(BaseViewSet):
project_member = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20
)
# Also create the issue property for the user
_ = IssueProperty.objects.create(
project_id=serializer.data["id"],
user=request.user,
)
if serializer.data["project_lead"] is not None and str(
serializer.data["project_lead"]
@@ -210,6 +214,11 @@ class ProjectViewSet(BaseViewSet):
member_id=serializer.data["project_lead"],
role=20,
)
# Also create the issue property for the user
IssueProperty.objects.create(
project_id=serializer.data["id"],
user_id=serializer.data["project_lead"],
)
# Default states
states = [
@@ -262,12 +271,9 @@ class ProjectViewSet(BaseViewSet):
]
)
data = serializer.data
# Additional fields of the member
data["sort_order"] = project_member.sort_order
data["member_role"] = project_member.role
data["is_member"] = True
return Response(data, status=status.HTTP_201_CREATED)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
serializer = ProjectListSerializer(project)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
@@ -317,6 +323,8 @@ class ProjectViewSet(BaseViewSet):
color="#ff7700",
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
serializer = ProjectListSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -326,7 +334,7 @@ class ProjectViewSet(BaseViewSet):
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
)
except Project.DoesNotExist or Workspace.DoesNotExist as e:
except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
)
@@ -393,6 +401,8 @@ class InviteProjectEndpoint(BaseAPIView):
member=user, project_id=project_id, role=role
)
_ = IssueProperty.objects.create(user=user, project_id=project_id)
return Response(
ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK
)
@@ -428,6 +438,18 @@ class UserProjectInvitationsViewset(BaseViewSet):
]
)
IssueProperty.objects.bulk_create(
[
ProjectMember(
project=invitation.project,
workspace=invitation.project.workspace,
user=request.user,
created_by=request.user,
)
for invitation in project_invitations
]
)
# Delete joined project invites
project_invitations.delete()
@@ -458,6 +480,83 @@ class ProjectMemberViewSet(BaseViewSet):
.select_related("workspace", "workspace__owner")
)
def create(self, request, slug, project_id):
members = request.data.get("members", [])
# get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
if not len(members):
return Response(
{"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST,
)
bulk_project_members = []
bulk_issue_props = []
project_members = (
ProjectMember.objects.filter(
workspace__slug=slug,
member_id__in=[member.get("member_id") for member in members],
)
.values("member_id", "sort_order")
.order_by("sort_order")
)
for member in members:
sort_order = [
project_member.get("sort_order")
for project_member in project_members
if str(project_member.get("member_id")) == str(member.get("member_id"))
]
bulk_project_members.append(
ProjectMember(
member_id=member.get("member_id"),
role=member.get("role", 10),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
)
)
bulk_issue_props.append(
IssueProperty(
user_id=member.get("member_id"),
project_id=project_id,
workspace_id=project.workspace_id,
)
)
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10,
ignore_conflicts=True,
)
_ = IssueProperty.objects.bulk_create(
bulk_issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, slug, project_id):
project_member = ProjectMember.objects.get(
member=request.user, workspace__slug=slug, project_id=project_id
)
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
member__is_bot=False,
).select_related("project", "member", "workspace")
if project_member.role > 10:
serializer = ProjectMemberAdminSerializer(project_members, many=True)
else:
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk):
project_member = ProjectMember.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
@@ -543,59 +642,6 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class AddMemberToProjectEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
def post(self, request, slug, project_id):
members = request.data.get("members", [])
# get the project
project = Project.objects.get(pk=project_id, workspace__slug=slug)
if not len(members):
return Response(
{"error": "Atleast one member is required"},
status=status.HTTP_400_BAD_REQUEST,
)
bulk_project_members = []
project_members = (
ProjectMember.objects.filter(
workspace__slug=slug,
member_id__in=[member.get("member_id") for member in members],
)
.values("member_id", "sort_order")
.order_by("sort_order")
)
for member in members:
sort_order = [
project_member.get("sort_order")
for project_member in project_members
if str(project_member.get("member_id")) == str(member.get("member_id"))
]
bulk_project_members.append(
ProjectMember(
member_id=member.get("member_id"),
role=member.get("role", 10),
project_id=project_id,
workspace_id=project.workspace_id,
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
)
)
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10,
ignore_conflicts=True,
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class AddTeamToProjectEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
@@ -614,6 +660,7 @@ class AddTeamToProjectEndpoint(BaseAPIView):
workspace = Workspace.objects.get(slug=slug)
project_members = []
issue_props = []
for member in team_members:
project_members.append(
ProjectMember(
@@ -623,11 +670,23 @@ class AddTeamToProjectEndpoint(BaseAPIView):
created_by=request.user,
)
)
issue_props.append(
IssueProperty(
project_id=project_id,
user_id=member,
workspace=workspace,
created_by=request.user,
)
)
ProjectMember.objects.bulk_create(
project_members, batch_size=10, ignore_conflicts=True
)
_ = IssueProperty.objects.bulk_create(
issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -743,6 +802,19 @@ class ProjectJoinEndpoint(BaseAPIView):
ignore_conflicts=True,
)
IssueProperty.objects.bulk_create(
[
IssueProperty(
project_id=project_id,
user=request.user,
workspace=workspace,
created_by=request.user,
)
for project_id in project_ids
],
ignore_conflicts=True,
)
return Response(
{"message": "Projects joined successfully"},
status=status.HTTP_201_CREATED,
@@ -869,21 +941,6 @@ class ProjectDeployBoardViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
class ProjectMemberEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id):
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
member__is_bot=False,
).select_related("project", "member", "workspace")
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
permission_classes = [
AllowAny,

View File

@@ -19,7 +19,6 @@ from plane.db.models import (
WorkspaceMemberInvite,
Issue,
IssueActivity,
WorkspaceMember,
)
from plane.utils.paginator import BasePaginator

View File

@@ -93,7 +93,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
)
)
@method_decorator(gzip_page)
def list(self, request, slug):
filters = issue_filters(request.query_params, "GET")
@@ -117,9 +116,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -129,9 +126,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
priority_order if order_by_param == "priority" else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
@@ -183,7 +178,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
@@ -194,10 +188,12 @@ class GlobalViewIssuesViewSet(BaseViewSet):
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
grouped_results = group_results(issues, group_by, sub_group_by)
return Response(
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
grouped_results,
status=status.HTTP_200_OK,
)
return Response(issues, status=status.HTTP_200_OK)

View File

@@ -6,12 +6,10 @@ from uuid import uuid4
# Django imports
from django.db import IntegrityError
from django.db.models import Prefetch
from django.conf import settings
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.contrib.sites.shortcuts import get_current_site
from django.db.models import (
Prefetch,
OuterRef,
@@ -55,7 +53,6 @@ from . import BaseViewSet
from plane.db.models import (
User,
Workspace,
WorkspaceMember,
WorkspaceMemberInvite,
Team,
ProjectMember,
@@ -472,7 +469,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
model = WorkspaceMember
permission_classes = [
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
]
search_fields = [
@@ -489,6 +486,25 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member")
)
def list(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
member=request.user, workspace__slug=slug
)
workspace_members = WorkspaceMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
).select_related("workspace", "member")
if workspace_member.role > 10:
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
else:
serializer = WorkSpaceMemberSerializer(
workspace_members,
many=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug)
if request.user.id == workspace_member.member_id:
@@ -1228,9 +1244,15 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
## Grouping the results
group_by = request.GET.get("group_by", False)
if group_by:
return Response(group_results(issues, group_by), status=status.HTTP_200_OK)
grouped_results = group_results(issues, group_by)
return Response(
grouped_results,
status=status.HTTP_200_OK,
)
return Response(issues, status=status.HTTP_200_OK)
return Response(
issues, status=status.HTTP_200_OK
)
class WorkspaceLabelsEndpoint(BaseAPIView):
@@ -1246,20 +1268,6 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
return Response(labels, status=status.HTTP_200_OK)
class WorkspaceMembersEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
workspace_members = WorkspaceMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
).select_related("workspace", "member")
serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
return Response(serialzier.data, status=status.HTTP_200_OK)
class LeaveWorkspaceEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,

View File

@@ -408,7 +408,6 @@ def analytic_export_task(email, data, slug):
distribution,
x_axis,
y_axis,
segment,
key,
assignee_details,
label_details,

View File

@@ -23,7 +23,7 @@ def email_verification(first_name, email, token, current_site):
from_email_string = settings.EMAIL_FROM
subject = f"Verify your Email!"
subject = "Verify your Email!"
context = {
"first_name": first_name,

View File

@@ -4,7 +4,6 @@ import io
import json
import boto3
import zipfile
from urllib.parse import urlparse, urlunparse
# Django imports
from django.conf import settings

View File

@@ -8,8 +8,6 @@ from django.conf import settings
from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.db.models import User
@shared_task
@@ -21,7 +19,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
from_email_string = settings.EMAIL_FROM
subject = f"Reset Your Password - Plane"
subject = "Reset Your Password - Plane"
context = {
"first_name": first_name,

View File

@@ -2,8 +2,6 @@
import json
import requests
import uuid
import jwt
from datetime import datetime
# Django imports
from django.conf import settings
@@ -25,8 +23,8 @@ from plane.db.models import (
WorkspaceIntegration,
Label,
User,
IssueProperty,
)
from .workspace_invitation_task import workspace_invitation
from plane.bgtasks.user_welcome_task import send_welcome_slack
@@ -57,7 +55,7 @@ def service_importer(service, importer_id):
ignore_conflicts=True,
)
[
_ = [
send_welcome_slack.delay(
str(user.id),
True,
@@ -103,6 +101,20 @@ def service_importer(service, importer_id):
ignore_conflicts=True,
)
IssueProperty.objects.bulk_create(
[
IssueProperty(
project_id=importer.project_id,
workspace_id=importer.workspace_id,
user=user,
created_by=importer.created_by,
)
for user in workspace_users
],
batch_size=100,
ignore_conflicts=True,
)
# Check if sync config is on for github importers
if service == "github" and importer.config.get("sync", False):
name = importer.metadata.get("name", False)
@@ -142,7 +154,7 @@ def service_importer(service, importer_id):
)
# Create repo sync
repo_sync = GithubRepositorySync.objects.create(
_ = GithubRepositorySync.objects.create(
repository=repo,
workspace_integration=workspace_integration,
actor=workspace_integration.actor,
@@ -164,7 +176,7 @@ def service_importer(service, importer_id):
ImporterSerializer(importer).data,
cls=DjangoJSONEncoder,
)
res = requests.post(
_ = requests.post(
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/",
json=import_data_json,
headers=headers,

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,7 @@ def archive_old_issues():
# Check if Issues
if issues:
# Set the archive time to current time
archive_at = timezone.now()
archive_at = timezone.now().date()
issues_to_update = []
for issue in issues:
@@ -71,14 +71,14 @@ def archive_old_issues():
Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
)
[
_ = [
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(archive_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
current_instance=json.dumps({"archived_at": None}),
subscriber=False,
epoch=int(timezone.now().timestamp())
)

View File

@@ -17,7 +17,7 @@ def magic_link(email, key, token, current_site):
from_email_string = settings.EMAIL_FROM
subject = f"Login for Plane"
subject = "Login for Plane"
context = {"magic_url": abs_url, "code": token}

View File

@@ -0,0 +1,274 @@
# Python imports
import json
# Django imports
from django.utils import timezone
# Module imports
from plane.db.models import (
IssueMention,
IssueSubscriber,
Project,
User,
IssueAssignee,
Issue,
Notification,
IssueComment,
)
# Third Party imports
from celery import shared_task
from bs4 import BeautifulSoup
def get_new_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database
# extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance)
# Getting Set Difference from mentions_newer
new_mentions = [
mention for mention in mentions_newer if mention not in mentions_older]
return new_mentions
# Get Removed Mention
def get_removed_mentions(requested_instance, current_instance):
# requested_data is the newer instance of the current issue
# current_instance is the older instance of the current issue, saved in the database
# extract mentions from both the instance of data
mentions_older = extract_mentions(current_instance)
mentions_newer = extract_mentions(requested_instance)
# Getting Set Difference from mentions_newer
removed_mentions = [
mention for mention in mentions_older if mention not in mentions_newer]
return removed_mentions
# Adds mentions as subscribers
def extract_mentions_as_subscribers(project_id, issue_id, mentions):
# mentions is an array of User IDs representing the FILTERED set of mentioned users
bulk_mention_subscribers = []
for mention_id in mentions:
# If the particular mention has not already been subscribed to the issue, he must be sent the mentioned notification
if not IssueSubscriber.objects.filter(
issue_id=issue_id,
subscriber=mention_id,
project=project_id,
).exists():
mentioned_user = User.objects.get(pk=mention_id)
project = Project.objects.get(pk=project_id)
issue = Issue.objects.get(pk=issue_id)
bulk_mention_subscribers.append(IssueSubscriber(
workspace=project.workspace,
project=project,
issue=issue,
subscriber=mentioned_user,
))
return bulk_mention_subscribers
# Parse Issue Description & extracts mentions
def extract_mentions(issue_instance):
try:
# issue_instance has to be a dictionary passed, containing the description_html and other set of activity data.
mentions = []
# Convert string to dictionary
data = json.loads(issue_instance)
html = data.get("description_html")
soup = BeautifulSoup(html, 'html.parser')
mention_tags = soup.find_all(
'mention-component', attrs={'target': 'users'})
mentions = [mention_tag['id'] for mention_tag in mention_tags]
return list(set(mentions))
except Exception as e:
return []
@shared_task
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance):
issue_activities_created = (
json.loads(
issue_activities_created) if issue_activities_created is not None else None
)
if type not in [
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
"issue_reaction.activity.created",
"issue_reaction.activity.deleted",
"comment_reaction.activity.created",
"comment_reaction.activity.deleted",
"issue_vote.activity.created",
"issue_vote.activity.deleted",
"issue_draft.activity.created",
"issue_draft.activity.updated",
"issue_draft.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
"""
Mention Tasks
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
"""
# Get new mentions from the newer instance
new_mentions = get_new_mentions(
requested_instance=requested_data, current_instance=current_instance)
removed_mention = get_removed_mentions(
requested_instance=requested_data, current_instance=current_instance)
# Get New Subscribers from the mentions of the newer instance
requested_mentions = extract_mentions(
issue_instance=requested_data)
mention_subscribers = extract_mentions_as_subscribers(
project_id=project_id, issue_id=issue_id, mentions=requested_mentions)
issue_subscribers = list(
IssueSubscriber.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(subscriber_id__in=list(new_mentions + [actor_id]))
.values_list("subscriber", flat=True)
)
issue_assignees = list(
IssueAssignee.objects.filter(
project_id=project_id, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
issue_subscribers = issue_subscribers + issue_assignees
issue = Issue.objects.filter(pk=issue_id).first()
if subscriber:
# add the user to issue subscriber
try:
_ = IssueSubscriber.objects.get_or_create(
issue_id=issue_id, subscriber_id=actor_id
)
except Exception as e:
pass
project = Project.objects.get(pk=project_id)
for subscriber in list(set(issue_subscribers)):
for issue_activity in issue_activities_created:
issue_comment = issue_activity.get("issue_comment")
if issue_comment is not None:
issue_comment = IssueComment.objects.get(id=issue_comment, issue_id=issue_id, project_id=project_id, workspace_id=project.workspace_id)
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities",
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
title=issue_activity.get("comment"),
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
"actor": str(issue_activity.get("actor_id")),
"new_value": str(issue_activity.get("new_value")),
"old_value": str(issue_activity.get("old_value")),
"issue_comment": str(
issue_comment.comment_stripped
if issue_activity.get("issue_comment") is not None
else ""
),
},
},
)
)
# Add Mentioned as Issue Subscribers
IssueSubscriber.objects.bulk_create(
mention_subscribers, batch_size=100)
for mention_id in new_mentions:
if (mention_id != actor_id):
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities:mention",
triggered_by_id=actor_id,
receiver_id=mention_id,
entity_identifier=issue_id,
entity_name="issue",
project=project,
message=f"You have been mentioned in the issue {issue.name}",
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(issue.project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.get("id")),
"verb": str(issue_activity.get("verb")),
"field": str(issue_activity.get("field")),
"actor": str(issue_activity.get("actor_id")),
"new_value": str(issue_activity.get("new_value")),
"old_value": str(issue_activity.get("old_value")),
},
},
)
)
# Create New Mentions Here
aggregated_issue_mentions = []
for mention_id in new_mentions:
mentioned_user = User.objects.get(pk=mention_id)
aggregated_issue_mentions.append(
IssueMention(
mention=mentioned_user,
issue=issue,
project=project,
workspace=project.workspace
)
)
IssueMention.objects.bulk_create(
aggregated_issue_mentions, batch_size=100)
IssueMention.objects.filter(
issue=issue, mention__in=removed_mention).delete()
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)

View File

@@ -11,7 +11,7 @@ from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
# Module imports
from plane.db.models import Workspace, User, WorkspaceMemberInvite
from plane.db.models import Workspace, WorkspaceMemberInvite
@shared_task

View File

@@ -29,4 +29,4 @@ app.conf.beat_schedule = {
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler'
app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler'

View File

@@ -0,0 +1,41 @@
# Generated by Django 4.2.5 on 2023-10-18 12:04
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.issue
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'),
]
operations = [
migrations.CreateModel(
name="issue_mentions",
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4,editable=False, primary_key=True, serialize=False, unique=True)),
('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,related_name='issuemention_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuemention', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuemention_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issuemention', to='db.workspace')),
],
options={
'verbose_name': 'IssueMention',
'verbose_name_plural': 'IssueMentions',
'db_table': 'issue_mentions',
'ordering': ('-created_at',),
},
),
migrations.AlterField(
model_name='issueproperty',
name='properties',
field=models.JSONField(default=plane.db.models.issue.get_default_properties),
),
]

View File

@@ -27,12 +27,12 @@ from .issue import (
IssueActivity,
IssueProperty,
IssueComment,
IssueBlocker,
IssueLabel,
IssueAssignee,
Label,
IssueBlocker,
IssueRelation,
IssueMention,
IssueLink,
IssueSequence,
IssueAttachment,
@@ -78,4 +78,4 @@ from .analytic import AnalyticView
from .notification import Notification
from .exporter import ExporterHistory
from .exporter import ExporterHistory

View File

@@ -53,4 +53,4 @@ class ExporterHistory(BaseModel):
def __str__(self):
"""Return name of the service"""
return f"{self.provider} <{self.workspace.name}>"
return f"{self.provider} <{self.workspace.name}>"

View File

@@ -1,3 +1,3 @@
from .base import Integration, WorkspaceIntegration
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
from .slack import SlackProjectSync
from .slack import SlackProjectSync

View File

@@ -6,7 +6,6 @@ from django.db import models
# Module imports
from plane.db.models import ProjectBaseModel
from plane.db.mixins import AuditModel
class GithubRepository(ProjectBaseModel):

View File

@@ -16,6 +16,24 @@ from . import ProjectBaseModel
from plane.utils.html_processor import strip_tags
def get_default_properties():
return {
"assignee": True,
"start_date": True,
"due_date": True,
"labels": True,
"key": True,
"priority": True,
"state": True,
"sub_issue_count": True,
"link": True,
"attachment_count": True,
"estimate": True,
"created_on": True,
"updated_on": True,
}
# TODO: Handle identifiers for Bulk Inserts - nk
class IssueManager(models.Manager):
def get_queryset(self):
@@ -39,7 +57,7 @@ class Issue(ProjectBaseModel):
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("none", "None")
("none", "None"),
)
parent = models.ForeignKey(
"self",
@@ -186,7 +204,7 @@ class IssueRelation(ProjectBaseModel):
("relates_to", "Relates To"),
("blocked_by", "Blocked By"),
)
issue = models.ForeignKey(
Issue, related_name="issue_relation", on_delete=models.CASCADE
)
@@ -209,6 +227,25 @@ class IssueRelation(ProjectBaseModel):
def __str__(self):
return f"{self.issue.name} {self.related_issue.name}"
class IssueMention(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_mention"
)
mention = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_mention",
)
class Meta:
unique_together = ["issue", "mention"]
verbose_name = "Issue Mention"
verbose_name_plural = "Issue Mentions"
db_table = "issue_mentions"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.mention.email}"
class IssueAssignee(ProjectBaseModel):
@@ -327,7 +364,9 @@ class IssueComment(ProjectBaseModel):
comment_json = models.JSONField(blank=True, default=dict)
comment_html = models.TextField(blank=True, default="<p></p>")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_comments"
)
# System can also create comment
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -367,7 +406,7 @@ class IssueProperty(ProjectBaseModel):
on_delete=models.CASCADE,
related_name="issue_property_user",
)
properties = models.JSONField(default=dict)
properties = models.JSONField(default=get_default_properties)
class Meta:
verbose_name = "Issue Property"
@@ -515,7 +554,10 @@ class IssueVote(ProjectBaseModel):
)
class Meta:
unique_together = ["issue", "actor",]
unique_together = [
"issue",
"actor",
]
verbose_name = "Issue Vote"
verbose_name_plural = "Issue Votes"
db_table = "issue_votes"

View File

@@ -4,9 +4,6 @@ from uuid import uuid4
# Django imports
from django.db import models
from django.conf import settings
from django.template.defaultfilters import slugify
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.validators import MinValueValidator, MaxValueValidator
# Modeule imports

View File

@@ -1,33 +0,0 @@
import jwt
import pytz
from django.conf import settings
from django.utils import timezone
from plane.db.models import User
class UserMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
if request.headers.get("Authorization"):
authorization_header = request.headers.get("Authorization")
access_token = authorization_header.split(" ")[1]
decoded = jwt.decode(
access_token, settings.SECRET_KEY, algorithms=["HS256"]
)
id = decoded['user_id']
user = User.objects.get(id=id)
user.last_active = timezone.now()
user.token_updated_at = None
user.save()
timezone.activate(pytz.timezone(user.user_timezone))
except Exception as e:
print(e)
response = self.get_response(request)
return response

View File

@@ -14,19 +14,21 @@ from .common import * # noqa
# Database
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "plane",
"USER": os.environ.get("PGUSER", ""),
"PASSWORD": os.environ.get("PGPASSWORD", ""),
"HOST": os.environ.get("PGHOST", ""),
if bool(os.environ.get("DATABASE_URL")):
# Parse database configuration from $DATABASE_URL
DATABASES["default"] = dj_database_url.config()
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("POSTGRES_DB"),
"USER": os.environ.get("POSTGRES_USER"),
"PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
"HOST": os.environ.get("POSTGRES_HOST"),
}
}
}
# Parse database configuration from $DATABASE_URL
DATABASES["default"] = dj_database_url.config()
SITE_ID = 1
# Set the variable true if running in docker environment
@@ -278,4 +280,3 @@ SCOUT_NAME = "Plane"
# Unsplash Access key
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY")

View File

@@ -4,7 +4,6 @@ import ssl
import certifi
import dj_database_url
from urllib.parse import urlparse
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

View File

@@ -1 +1 @@
from .api import *
from .api import *

View File

@@ -2,16 +2,13 @@
"""
# from django.contrib import admin
from django.urls import path, include, re_path
from django.views.generic import TemplateView
from django.conf import settings
# from django.conf.urls.static import static
urlpatterns = [
# path("admin/", admin.site.urls),
path("", TemplateView.as_view(template_name="index.html")),
path("api/", include("plane.api.urls")),
path("", include("plane.web.urls")),

View File

@@ -12,19 +12,19 @@ from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear, Conc
from plane.db.models import Issue
def annotate_with_monthly_dimension(queryset, field_name):
def annotate_with_monthly_dimension(queryset, field_name, attribute):
# Get the year and the months
year = ExtractYear(field_name)
month = ExtractMonth(field_name)
# Concat the year and month
dimension = Concat(year, Value("-"), month, output_field=CharField())
# Annotate the dimension
return queryset.annotate(dimension=dimension)
return queryset.annotate(**{attribute: dimension})
def extract_axis(queryset, x_axis):
# Format the dimension when the axis is in date
if x_axis in ["created_at", "start_date", "target_date", "completed_at"]:
queryset = annotate_with_monthly_dimension(queryset, x_axis)
queryset = annotate_with_monthly_dimension(queryset, x_axis, "dimension")
return queryset, "dimension"
else:
return queryset.annotate(dimension=F(x_axis)), "dimension"
@@ -47,7 +47,7 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
#
if segment in ["created_at", "start_date", "target_date", "completed_at"]:
queryset = annotate_with_monthly_dimension(queryset, segment)
queryset = annotate_with_monthly_dimension(queryset, segment, "segmented")
segment = "segmented"
queryset = queryset.values(x_axis)
@@ -81,7 +81,6 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
# Total Issues in Cycle or Module
total_issues = queryset.total_issues
if cycle_id:
# Get all dates between the two dates
date_range = [
@@ -103,7 +102,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
.values("date", "total_completed")
.order_by("date")
)
if module_id:
# Get all dates between the two dates
date_range = [
@@ -126,18 +125,15 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
.order_by("date")
)
for date in date_range:
cumulative_pending_issues = total_issues
total_completed = 0
total_completed = sum(
[
item["total_completed"]
for item in completed_issues_distribution
if item["date"] is not None and item["date"] <= date
]
item["total_completed"]
for item in completed_issues_distribution
if item["date"] is not None and item["date"] <= date
)
cumulative_pending_issues -= total_completed
chart_data[str(date)] = cumulative_pending_issues
return chart_data
return chart_data

View File

@@ -127,7 +127,7 @@ def group_results(results_data, group_by, sub_group_by=False):
return main_responsive_dict
else:
response_dict = dict()
response_dict = {}
if group_by == "priority":
response_dict = {

View File

@@ -17,4 +17,4 @@ def import_submodules(context, root_module, path):
for k, v in six.iteritems(vars(module)):
if not k.startswith('_'):
context[k] = v
context[module_name] = module
context[module_name] = module

View File

@@ -0,0 +1,20 @@
import os
import requests
def slack_oauth(code):
SLACK_OAUTH_URL = os.environ.get("SLACK_OAUTH_URL", False)
SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", False)
SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET", False)
# Oauth Slack
if SLACK_OAUTH_URL and SLACK_CLIENT_ID and SLACK_CLIENT_SECRET:
response = requests.get(
SLACK_OAUTH_URL,
params={
"code": code,
"client_id": SLACK_CLIENT_ID,
"client_secret": SLACK_CLIENT_SECRET,
},
)
return response.json()
return {}

View File

@@ -4,4 +4,4 @@ def get_client_ip(request):
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
return ip

View File

@@ -1,10 +1,24 @@
import re
import uuid
from datetime import timedelta
from django.utils import timezone
# The date from pattern
pattern = re.compile(r"\d+_(weeks|months)$")
# check the valid uuids
def filter_valid_uuids(uuid_list):
valid_uuids = []
for uuid_str in uuid_list:
try:
uuid_obj = uuid.UUID(uuid_str)
valid_uuids.append(uuid_obj)
except ValueError:
# ignore the invalid uuids
pass
return valid_uuids
# Get the 2_weeks, 3_months
def string_date_filter(filter, duration, subsequent, term, date_filter, offset):
@@ -61,40 +75,41 @@ def date_filter(filter, date_term, queries):
def filter_state(params, filter, method):
if method == "GET":
states = params.get("state").split(",")
states = [item for item in params.get("state").split(",") if item != 'null']
states = filter_valid_uuids(states)
if len(states) and "" not in states:
filter["state__in"] = states
else:
if params.get("state", None) and len(params.get("state")):
if params.get("state", None) and len(params.get("state")) and params.get("state") != 'null':
filter["state__in"] = params.get("state")
return filter
def filter_state_group(params, filter, method):
if method == "GET":
state_group = params.get("state_group").split(",")
state_group = [item for item in params.get("state_group").split(",") if item != 'null']
if len(state_group) and "" not in state_group:
filter["state__group__in"] = state_group
else:
if params.get("state_group", None) and len(params.get("state_group")):
if params.get("state_group", None) and len(params.get("state_group")) and params.get("state_group") != 'null':
filter["state__group__in"] = params.get("state_group")
return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
estimate_points = [item for item in params.get("estimate_point").split(",") if item != 'null']
if len(estimate_points) and "" not in estimate_points:
filter["estimate_point__in"] = estimate_points
else:
if params.get("estimate_point", None) and len(params.get("estimate_point")):
if params.get("estimate_point", None) and len(params.get("estimate_point")) and params.get("estimate_point") != 'null':
filter["estimate_point__in"] = params.get("estimate_point")
return filter
def filter_priority(params, filter, method):
if method == "GET":
priorities = params.get("priority").split(",")
priorities = [item for item in params.get("priority").split(",") if item != 'null']
if len(priorities) and "" not in priorities:
filter["priority__in"] = priorities
return filter
@@ -102,44 +117,59 @@ def filter_priority(params, filter, method):
def filter_parent(params, filter, method):
if method == "GET":
parents = params.get("parent").split(",")
parents = [item for item in params.get("parent").split(",") if item != 'null']
parents = filter_valid_uuids(parents)
if len(parents) and "" not in parents:
filter["parent__in"] = parents
else:
if params.get("parent", None) and len(params.get("parent")):
if params.get("parent", None) and len(params.get("parent")) and params.get("parent") != 'null':
filter["parent__in"] = params.get("parent")
return filter
def filter_labels(params, filter, method):
if method == "GET":
labels = params.get("labels").split(",")
labels = [item for item in params.get("labels").split(",") if item != 'null']
labels = filter_valid_uuids(labels)
if len(labels) and "" not in labels:
filter["labels__in"] = labels
else:
if params.get("labels", None) and len(params.get("labels")):
if params.get("labels", None) and len(params.get("labels")) and params.get("labels") != 'null':
filter["labels__in"] = params.get("labels")
return filter
def filter_assignees(params, filter, method):
if method == "GET":
assignees = params.get("assignees").split(",")
assignees = [item for item in params.get("assignees").split(",") if item != 'null']
assignees = filter_valid_uuids(assignees)
if len(assignees) and "" not in assignees:
filter["assignees__in"] = assignees
else:
if params.get("assignees", None) and len(params.get("assignees")):
if params.get("assignees", None) and len(params.get("assignees")) and params.get("assignees") != 'null':
filter["assignees__in"] = params.get("assignees")
return filter
def filter_mentions(params, filter, method):
if method == "GET":
mentions = [item for item in params.get("mentions").split(",") if item != 'null']
mentions = filter_valid_uuids(mentions)
if len(mentions) and "" not in mentions:
filter["issue_mention__mention__id__in"] = mentions
else:
if params.get("mentions", None) and len(params.get("mentions")) and params.get("mentions") != 'null':
filter["issue_mention__mention__id__in"] = params.get("mentions")
return filter
def filter_created_by(params, filter, method):
if method == "GET":
created_bys = params.get("created_by").split(",")
created_bys = [item for item in params.get("created_by").split(",") if item != 'null']
created_bys = filter_valid_uuids(created_bys)
if len(created_bys) and "" not in created_bys:
filter["created_by__in"] = created_bys
else:
if params.get("created_by", None) and len(params.get("created_by")):
if params.get("created_by", None) and len(params.get("created_by")) and params.get("created_by") != 'null':
filter["created_by__in"] = params.get("created_by")
return filter
@@ -179,7 +209,7 @@ def filter_start_date(params, filter, method):
date_filter(filter=filter, date_term="start_date", queries=start_dates)
else:
if params.get("start_date", None) and len(params.get("start_date")):
date_filter(filter=filter, date_term="start_date", queries=params.get("start_date", []))
filter["start_date"] = params.get("start_date")
return filter
@@ -190,7 +220,7 @@ def filter_target_date(params, filter, method):
date_filter(filter=filter, date_term="target_date", queries=target_dates)
else:
if params.get("target_date", None) and len(params.get("target_date")):
date_filter(filter=filter, date_term="target_date", queries=params.get("target_date", []))
filter["target_date"] = params.get("target_date")
return filter
@@ -219,44 +249,47 @@ def filter_issue_state_type(params, filter, method):
def filter_project(params, filter, method):
if method == "GET":
projects = params.get("project").split(",")
projects = [item for item in params.get("project").split(",") if item != 'null']
projects = filter_valid_uuids(projects)
if len(projects) and "" not in projects:
filter["project__in"] = projects
else:
if params.get("project", None) and len(params.get("project")):
if params.get("project", None) and len(params.get("project")) and params.get("project") != 'null':
filter["project__in"] = params.get("project")
return filter
def filter_cycle(params, filter, method):
if method == "GET":
cycles = params.get("cycle").split(",")
cycles = [item for item in params.get("cycle").split(",") if item != 'null']
cycles = filter_valid_uuids(cycles)
if len(cycles) and "" not in cycles:
filter["issue_cycle__cycle_id__in"] = cycles
else:
if params.get("cycle", None) and len(params.get("cycle")):
if params.get("cycle", None) and len(params.get("cycle")) and params.get("cycle") != 'null':
filter["issue_cycle__cycle_id__in"] = params.get("cycle")
return filter
def filter_module(params, filter, method):
if method == "GET":
modules = params.get("module").split(",")
modules = [item for item in params.get("module").split(",") if item != 'null']
modules = filter_valid_uuids(modules)
if len(modules) and "" not in modules:
filter["issue_module__module_id__in"] = modules
else:
if params.get("module", None) and len(params.get("module")):
if params.get("module", None) and len(params.get("module")) and params.get("module") != 'null':
filter["issue_module__module_id__in"] = params.get("module")
return filter
def filter_inbox_status(params, filter, method):
if method == "GET":
status = params.get("inbox_status").split(",")
status = [item for item in params.get("inbox_status").split(",") if item != 'null']
if len(status) and "" not in status:
filter["issue_inbox__status__in"] = status
else:
if params.get("inbox_status", None) and len(params.get("inbox_status")):
if params.get("inbox_status", None) and len(params.get("inbox_status")) and params.get("inbox_status") != 'null':
filter["issue_inbox__status__in"] = params.get("inbox_status")
return filter
@@ -275,11 +308,12 @@ def filter_sub_issue_toggle(params, filter, method):
def filter_subscribed_issues(params, filter, method):
if method == "GET":
subscribers = params.get("subscriber").split(",")
subscribers = [item for item in params.get("subscriber").split(",") if item != 'null']
subscribers = filter_valid_uuids(subscribers)
if len(subscribers) and "" not in subscribers:
filter["issue_subscribers__subscriber_id__in"] = subscribers
else:
if params.get("subscriber", None) and len(params.get("subscriber")):
if params.get("subscriber", None) and len(params.get("subscriber")) and params.get("subscriber") != 'null':
filter["issue_subscribers__subscriber_id__in"] = params.get("subscriber")
return filter
@@ -293,7 +327,7 @@ def filter_start_target_date_issues(params, filter, method):
def issue_filters(query_params, method):
filter = dict()
filter = {}
ISSUE_FILTER = {
"state": filter_state,
@@ -303,6 +337,7 @@ def issue_filters(query_params, method):
"parent": filter_parent,
"labels": filter_labels,
"assignees": filter_assignees,
"mentions": filter_mentions,
"created_by": filter_created_by,
"name": filter_name,
"created_at": filter_created_at,

View File

@@ -1,3 +1,3 @@
import mistune
markdown = mistune.Markdown()
markdown = mistune.Markdown()

View File

@@ -21,12 +21,7 @@ class Cursor:
)
def __repr__(self):
return "<{}: value={} offset={} is_prev={}>".format(
type(self).__name__,
self.value,
self.offset,
int(self.is_prev),
)
return f"{type(self).__name__,}: value={self.value} offset={self.offset}, is_prev={int(self.is_prev)}"
def __bool__(self):
return bool(self.has_results)
@@ -176,10 +171,6 @@ class BasePaginator:
**paginator_kwargs,
):
"""Paginate the request"""
assert (paginator and not paginator_kwargs) or (
paginator_cls and paginator_kwargs
)
per_page = self.get_per_page(request, default_per_page, max_per_page)
# Convert the cursor value to integer and float from string

View File

@@ -33,4 +33,5 @@ django_celery_beat==2.5.0
psycopg-binary==3.1.10
psycopg-c==3.1.10
scout-apm==2.26.1
openpyxl==3.1.2
openpyxl==3.1.2
beautifulsoup4==4.12.2

View File

@@ -1,4 +0,0 @@
# Deploy the Plane image
FROM makeplane/plane
LABEL maintainer="engineering@plane.so"

View File

@@ -0,0 +1,168 @@
version: "3.8"
x-app-env : &app-env
environment:
- NGINX_PORT=${NGINX_PORT:-84}
- DEBUG=${DEBUG:-0}
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces}
- SENTRY_DSN=${SENTRY_DSN:-""}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
- DOCKERIZED=${DOCKERIZED:-1}
#DB SETTINGS
- PGHOST=${PGHOST:-plane-db}
- PGDATABASE=${PGDATABASE:-plane}
- POSTGRES_USER=${POSTGRES_USER:-plane}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
- POSTGRES_DB=${POSTGRES_DB:-plane}
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}}
# REDIS SETTINGS
- REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
# EMAIL SETTINGS
- EMAIL_HOST=${EMAIL_HOST:-""}
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
- EMAIL_PORT=${EMAIL_PORT:-587}
- EMAIL_FROM=${EMAIL_FROM:-"Team Plane &lt;team@mailer.plane.so&gt;"}
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
# OPENAI SETTINGS
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
- OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"}
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
# LOGIN/SIGNUP SETTINGS
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
# DATA STORE SETTINGS
- USE_MINIO=${USE_MINIO:-1}
- AWS_REGION=${AWS_REGION:-""}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-"access-key"}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-"secret-key"}
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-"access-key"}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
services:
web:
<<: *app-env
platform: linux/amd64
image: makeplane/plane-frontend:${APP_RELEASE:-latest}
restart: unless-stopped
command: /usr/local/bin/start.sh web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
depends_on:
- api
- worker
space:
<<: *app-env
platform: linux/amd64
image: makeplane/plane-space:${APP_RELEASE:-latest}
restart: unless-stopped
command: /usr/local/bin/start.sh space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
depends_on:
- api
- worker
- web
api:
<<: *app-env
platform: linux/amd64
image: makeplane/plane-backend:${APP_RELEASE:-latest}
restart: unless-stopped
command: ./bin/takeoff
deploy:
replicas: ${API_REPLICAS:-1}
depends_on:
- plane-db
- plane-redis
worker:
<<: *app-env
container_name: bgworker
platform: linux/amd64
image: makeplane/plane-backend:${APP_RELEASE:-latest}
restart: unless-stopped
command: ./bin/worker
depends_on:
- api
- plane-db
- plane-redis
beat-worker:
<<: *app-env
container_name: beatworker
platform: linux/amd64
image: makeplane/plane-backend:${APP_RELEASE:-latest}
restart: unless-stopped
command: ./bin/beat
depends_on:
- api
- plane-db
- plane-redis
plane-db:
<<: *app-env
container_name: plane-db
image: postgres:15.2-alpine
restart: unless-stopped
command: postgres -c 'max_connections=1000'
volumes:
- pgdata:/var/lib/postgresql/data
plane-redis:
<<: *app-env
container_name: plane-redis
image: redis:6.2.7-alpine
restart: unless-stopped
volumes:
- redisdata:/data
plane-minio:
<<: *app-env
container_name: plane-minio
image: minio/minio
restart: unless-stopped
command: server /export --console-address ":9090"
volumes:
- uploads:/export
createbuckets:
<<: *app-env
image: minio/mc
entrypoint: >
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
depends_on:
- plane-minio
# Comment this if you already have a reverse proxy running
proxy:
<<: *app-env
container_name: proxy
platform: linux/amd64
image: makeplane/plane-proxy:${APP_RELEASE:-latest}
ports:
- ${NGINX_PORT}:80
depends_on:
- web
- api
- space
volumes:
pgdata:
redisdata:
uploads:

111
deploy/selfhost/install.sh Executable file
View File

@@ -0,0 +1,111 @@
#!/bin/bash
BRANCH=${BRANCH:-master}
SCRIPT_DIR=$PWD
PLANE_INSTALL_DIR=$PWD/plane-app
mkdir -p $PLANE_INSTALL_DIR/archive
function install(){
echo
echo "Installing on $PLANE_INSTALL_DIR"
download
}
function download(){
cd $SCRIPT_DIR
TS=$(date +%s)
if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
then
mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
fi
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/docker-compose.yaml https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/variables-upgrade.env https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
if [ -f "$PLANE_INSTALL_DIR/.env" ];
then
cp $PLANE_INSTALL_DIR/.env $PLANE_INSTALL_DIR/archive/$TS.env
else
mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
fi
echo ""
echo "Latest version is now available for you to use"
echo ""
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
echo ""
}
function startServices(){
cd $PLANE_INSTALL_DIR
docker compose up -d
cd $SCRIPT_DIR
}
function stopServices(){
cd $PLANE_INSTALL_DIR
docker compose down
cd $SCRIPT_DIR
}
function restartServices(){
cd $PLANE_INSTALL_DIR
docker compose restart
cd $SCRIPT_DIR
}
function upgrade(){
echo "***** STOPPING SERVICES ****"
stopServices
echo
echo "***** DOWNLOADING LATEST VERSION ****"
download
echo "***** PLEASE VALIDATE AND START SERVICES ****"
}
function askForAction(){
echo
echo "Select a Action you want to perform:"
echo " 1) Install"
echo " 2) Start"
echo " 3) Stop"
echo " 4) Restart"
echo " 5) Upgrade"
echo " 6) Exit"
echo
read -p "Action [2]: " ACTION
until [[ -z "$ACTION" || "$ACTION" =~ ^[1-6]$ ]]; do
echo "$ACTION: invalid selection."
read -p "Action [2]: " ACTION
done
echo
if [ "$ACTION" == "1" ]
then
install
askForAction
elif [ "$ACTION" == "2" ] || [ "$ACTION" == "" ]
then
startServices
askForAction
elif [ "$ACTION" == "3" ]
then
stopServices
askForAction
elif [ "$ACTION" == "4" ]
then
restartServices
askForAction
elif [ "$ACTION" == "5" ]
then
upgrade
askForAction
elif [ "$ACTION" == "6" ]
then
exit 0
else
echo "INVALID ACTION SUPPLIED"
fi
}
askForAction

View File

@@ -0,0 +1,63 @@
APP_RELEASE=latest
WEB_REPLICAS=1
SPACE_REPLICAS=1
API_REPLICAS=1
NGINX_PORT=80
DEBUG=0
DJANGO_SETTINGS_MODULE=plane.settings.selfhosted
NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
SENTRY_DSN=""
GITHUB_CLIENT_SECRET=""
DOCKERIZED=1
#DB SETTINGS
PGHOST=plane-db
PGDATABASE=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=plane
POSTGRES_DB=plane
PGDATA=/var/lib/postgresql/data
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}
# REDIS SETTINGS
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=redis://${REDIS_HOST}:6379/
# EMAIL SETTINGS
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane &lt;team@mailer.plane.so&gt;"
EMAIL_USE_TLS=1
EMAIL_USE_SSL=0
DEFAULT_EMAIL=captain@plane.so
DEFAULT_PASSWORD=password123
# OPENAI SETTINGS
OPENAI_API_BASE=https://api.openai.com/v1
OPENAI_API_KEY="sk-"
GPT_ENGINE="gpt-3.5-turbo"
# LOGIN/SIGNUP SETTINGS
ENABLE_SIGNUP=1
ENABLE_EMAIL_PASSWORD=1
ENABLE_MAGIC_LINK_LOGIN=0
SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
# DATA STORE SETTINGS
USE_MINIO=1
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
MINIO_ROOT_USER="access-key"
MINIO_ROOT_PASSWORD="secret-key"
BUCKET_NAME=uploads
FILE_SIZE_LIMIT=5242880

View File

@@ -1,234 +0,0 @@
version: "3.8"
services:
web:
container_name: web
platform: linux/amd64
image: makeplane/plane-frontend:latest
restart: always
command: /usr/local/bin/start.sh web/server.js web
environment:
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces}
depends_on:
- api
- worker
space:
container_name: space
platform: linux/amd64
image: makeplane/plane-space:latest
restart: always
command: /usr/local/bin/start.sh space/server.js space
environment:
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
depends_on:
- api
- worker
- web
api:
container_name: api
platform: linux/amd64
image: makeplane/plane-backend:latest
restart: always
command: ./bin/takeoff
environment:
- DEBUG=${DEBUG:-0}
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
- SENTRY_DSN=${SENTRY_DSN:-""}
- PGUSER=${PGUSER:-plane}
- PGPASSWORD=${PGPASSWORD:-plane}
- PGHOST=${PGHOST:-plane-db}
- PGDATABASE=${PGDATABASE:-plane}
- DATABASE_URL=${DATABASE_URL:-postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}}
- REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
- EMAIL_HOST=${EMAIL_HOST:-""}
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
- EMAIL_PORT=${EMAIL_PORT:-587}
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
- AWS_REGION=${AWS_REGION:-""}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
- DOCKERIZED=${DOCKERIZED:-1}
- USE_MINIO=${USE_MINIO:-1}
- NGINX_PORT=${NGINX_PORT:-80}
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
depends_on:
- plane-db
- plane-redis
worker:
container_name: bgworker
platform: linux/amd64
image: makeplane/plane-backend:latest
restart: always
command: ./bin/worker
environment:
- DEBUG=${DEBUG:-0}
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
- SENTRY_DSN=${SENTRY_DSN:-""}
- PGUSER=${PGUSER:-plane}
- PGPASSWORD=${PGPASSWORD:-plane}
- PGHOST=${PGHOST:-plane-db}
- PGDATABASE=${PGDATABASE:-plane}
- DATABASE_URL=${DATABASE_URL:-postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}}
- REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
- EMAIL_HOST=${EMAIL_HOST:-""}
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
- EMAIL_PORT=${EMAIL_PORT:-587}
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
- AWS_REGION=${AWS_REGION:-""}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
- DOCKERIZED=${DOCKERIZED:-1}
- USE_MINIO=${USE_MINIO:-1}
- NGINX_PORT=${NGINX_PORT:-80}
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
depends_on:
- api
- plane-db
- plane-redis
beat-worker:
container_name: beatworker
platform: linux/amd64
image: makeplane/plane-backend:latest
restart: always
command: ./bin/beat
environment:
- DEBUG=${DEBUG:-0}
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
- SENTRY_DSN=${SENTRY_DSN:-""}
- PGUSER=${PGUSER:-plane}
- PGPASSWORD=${PGPASSWORD:-plane}
- PGHOST=${PGHOST:-plane-db}
- PGDATABASE=${PGDATABASE:-plane}
- DATABASE_URL=${DATABASE_URL:-postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}}
- REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
- EMAIL_HOST=${EMAIL_HOST:-""}
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
- EMAIL_PORT=${EMAIL_PORT:-587}
- EMAIL_FROM=${EMAIL_FROM:-Team Plane <team@mailer.plane.so>}
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
- AWS_REGION=${AWS_REGION:-""}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-}
- GPT_ENGINE=${GPT_ENGINE:-gpt-3.5-turbo}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
- DOCKERIZED=${DOCKERIZED:-1}
- USE_MINIO=${USE_MINIO:-1}
- NGINX_PORT=${NGINX_PORT:-80}
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
depends_on:
- api
- plane-db
- plane-redis
plane-db:
container_name: plane-db
image: postgres:15.2-alpine
restart: always
command: postgres -c 'max_connections=1000'
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_USER:-plane}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
- POSTGRES_DB=${POSTGRES_DB:-plane}
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
plane-redis:
container_name: plane-redis
image: redis:6.2.7-alpine
restart: always
volumes:
- redisdata:/data
plane-minio:
container_name: plane-minio
image: minio/minio
restart: always
command: server /export --console-address ":9090"
volumes:
- uploads:/export
environment:
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-access-key}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-secret-key}
createbuckets:
image: minio/mc
entrypoint: >
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
environment:
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-access-key}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-secret-key}
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
depends_on:
- plane-minio
# Comment this if you already have a reverse proxy running
proxy:
container_name: proxy
platform: linux/amd64
image: makeplane/plane-proxy:latest
ports:
- ${NGINX_PORT}:80
environment:
- NGINX_PORT=${NGINX_PORT:-80}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
depends_on:
- web
- api
- space
volumes:
pgdata:
redisdata:
uploads:

View File

@@ -27,7 +27,7 @@
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"tailwindcss": "^3.3.3",
"turbo": "^1.10.14"
"turbo": "^1.10.16"
},
"resolutions": {
"@types/react": "18.2.0"

View File

@@ -2,6 +2,7 @@
"name": "@plane/editor-core",
"version": "0.0.1",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
@@ -21,10 +22,10 @@
"check-types": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "18.2.0",
"next": "12.3.2",
"next-themes": "^0.2.1"
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "18.2.0"
},
"dependencies": {
"@blueprintjs/popover2": "^2.0.10",
@@ -32,6 +33,7 @@
"@tiptap/extension-color": "^2.1.11",
"@tiptap/extension-image": "^2.1.7",
"@tiptap/extension-link": "^2.1.7",
"@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-table": "^2.1.6",
"@tiptap/extension-table-cell": "^2.1.6",
"@tiptap/extension-table-header": "^2.1.6",
@@ -40,12 +42,15 @@
"@tiptap/extension-task-list": "^2.1.7",
"@tiptap/extension-text-style": "^2.1.11",
"@tiptap/extension-underline": "^2.1.7",
"@tiptap/prosemirror-tables": "^1.1.4",
"jsx-dom-cjs": "^8.0.3",
"@tiptap/pm": "^2.1.7",
"@tiptap/react": "^2.1.7",
"@tiptap/starter-kit": "^2.1.10",
"@tiptap/suggestion": "^2.0.4",
"@types/node": "18.15.3",
"@types/react": "^18.2.5",
"@types/react-dom": "18.0.11",
"@types/node": "18.15.3",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"eslint": "8.36.0",
@@ -53,6 +58,7 @@
"eventsource-parser": "^0.1.0",
"lucide-react": "^0.244.0",
"react-markdown": "^8.0.7",
"react-moveable": "^0.54.2",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.2",

View File

@@ -2,8 +2,11 @@
// import "./styles/tailwind.css";
// import "./styles/editor.css";
export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
// utils
export * from "./lib/utils";
export * from "./ui/extensions/table/table";
export { startImageUpload } from "./ui/plugins/upload-image";
// components

View File

@@ -0,0 +1,10 @@
export type IMentionSuggestion = {
id: string;
type: string;
avatar: string;
title: string;
subtitle: string;
redirect_uri: string;
}
export type IMentionHighlight = string

View File

@@ -7,7 +7,11 @@ interface EditorContainerProps {
children: ReactNode;
}
export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => (
export const EditorContainer = ({
editor,
editorClassNames,
children,
}: EditorContainerProps) => (
<div
id="editor-container"
onClick={() => {

View File

@@ -1,7 +1,6 @@
import { Editor, EditorContent } from "@tiptap/react";
import { ReactNode } from "react";
import { ImageResizer } from "../extensions/image/image-resize";
import { TableMenu } from "../menus/table-menu";
interface EditorContentProps {
editor: Editor | null;
@@ -10,10 +9,8 @@ interface EditorContentProps {
}
export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = '', children }: EditorContentProps) => (
<div className={`${editorContentCustomClassNames}`}>
{/* @ts-ignore */}
<div className={`contentEditor ${editorContentCustomClassNames}`}>
<EditorContent editor={editor} />
{editor?.isEditable && <TableMenu editor={editor} />}
{(editor?.isActive("image") && editor?.isEditable) && <ImageResizer editor={editor} />}
{children}
</div>

View File

@@ -23,8 +23,8 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
origin={false}
edge={false}
throttleDrag={0}
keepRatio={true}
resizable={true}
keepRatio
resizable
throttleResize={0}
onResize={({ target, width, height, delta }: any) => {
delta[0] && (target!.style.width = `${width}px`);
@@ -33,7 +33,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
onResizeEnd={() => {
updateMediaSize();
}}
scalable={true}
scalable
renderDirections={["w", "e"]}
onScale={({ target, transform }: any) => {
target!.style.transform = transform;

View File

@@ -8,18 +8,21 @@ import TaskList from "@tiptap/extension-task-list";
import { Markdown } from "tiptap-markdown";
import Gapcursor from "@tiptap/extension-gapcursor";
import { CustomTableCell } from "./table/table-cell";
import { Table } from "./table";
import { TableHeader } from "./table/table-header";
import { TableRow } from "@tiptap/extension-table-row";
import TableHeader from "./table/table-header/table-header";
import Table from "./table/table";
import TableCell from "./table/table-cell/table-cell";
import TableRow from "./table/table-row/table-row";
import ImageExtension from "./image";
import { DeleteImage } from "../../types/delete-image";
import { isValidHttpUrl } from "../../lib/utils";
import { IMentionSuggestion } from "../../types/mention-suggestion";
import { Mentions } from "../mentions";
export const CoreEditorExtensions = (
mentionConfig: { mentionSuggestions: IMentionSuggestion[], mentionHighlights: string[] },
deleteFile: DeleteImage,
) => [
StarterKit.configure({
@@ -92,6 +95,7 @@ export const CoreEditorExtensions = (
}),
Table,
TableHeader,
CustomTableCell,
TableCell,
TableRow,
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
];

View File

@@ -1,9 +0,0 @@
import { Table as BaseTable } from "@tiptap/extension-table";
const Table = BaseTable.configure({
resizable: true,
cellMinWidth: 100,
allowTableNodeSelection: true,
});
export { Table };

View File

@@ -1,32 +0,0 @@
import { TableCell } from "@tiptap/extension-table-cell";
export const CustomTableCell = TableCell.extend({
addAttributes() {
return {
...this.parent?.(),
isHeader: {
default: false,
parseHTML: (element) => {
isHeader: element.tagName === "TD";
},
renderHTML: (attributes) => {
tag: attributes.isHeader ? "th" : "td";
},
},
};
},
renderHTML({ HTMLAttributes }) {
if (HTMLAttributes.isHeader) {
return [
"th",
{
...HTMLAttributes,
class: `relative ${HTMLAttributes.class}`,
},
["span", { class: "absolute top-0 right-0" }],
0,
];
}
return ["td", HTMLAttributes, 0];
},
});

View File

@@ -0,0 +1 @@
export { default as default } from "./table-cell"

View File

@@ -0,0 +1,58 @@
import { mergeAttributes, Node } from "@tiptap/core"
export interface TableCellOptions {
HTMLAttributes: Record<string, any>
}
export default Node.create<TableCellOptions>({
name: "tableCell",
addOptions() {
return {
HTMLAttributes: {}
}
},
content: "paragraph+",
addAttributes() {
return {
colspan: {
default: 1
},
rowspan: {
default: 1
},
colwidth: {
default: null,
parseHTML: (element) => {
const colwidth = element.getAttribute("colwidth")
const value = colwidth ? [parseInt(colwidth, 10)] : null
return value
}
},
background: {
default: "none"
}
}
},
tableRole: "cell",
isolating: true,
parseHTML() {
return [{ tag: "td" }]
},
renderHTML({ node, HTMLAttributes }) {
return [
"td",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
style: `background-color: ${node.attrs.background}`
}),
0
]
}
})

View File

@@ -1,7 +0,0 @@
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
const TableHeader = BaseTableHeader.extend({
content: "paragraph",
});
export { TableHeader };

View File

@@ -0,0 +1 @@
export { default as default } from "./table-header"

View File

@@ -0,0 +1,57 @@
import { mergeAttributes, Node } from "@tiptap/core"
export interface TableHeaderOptions {
HTMLAttributes: Record<string, any>
}
export default Node.create<TableHeaderOptions>({
name: "tableHeader",
addOptions() {
return {
HTMLAttributes: {}
}
},
content: "paragraph+",
addAttributes() {
return {
colspan: {
default: 1
},
rowspan: {
default: 1
},
colwidth: {
default: null,
parseHTML: (element) => {
const colwidth = element.getAttribute("colwidth")
const value = colwidth ? [parseInt(colwidth, 10)] : null
return value
}
},
background: {
default: "rgb(var(--color-primary-100))"
}
}
},
tableRole: "header_cell",
isolating: true,
parseHTML() {
return [{ tag: "th" }]
},
renderHTML({ node, HTMLAttributes }) {
return [
"th",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
style: `background-color: ${node.attrs.background}`
}),
0
]
}
})

View File

@@ -0,0 +1 @@
export { default as default } from "./table-row"

View File

@@ -0,0 +1,31 @@
import { mergeAttributes, Node } from "@tiptap/core"
export interface TableRowOptions {
HTMLAttributes: Record<string, any>
}
export default Node.create<TableRowOptions>({
name: "tableRow",
addOptions() {
return {
HTMLAttributes: {}
}
},
content: "(tableCell | tableHeader)*",
tableRole: "row",
parseHTML() {
return [{ tag: "tr" }]
},
renderHTML({ HTMLAttributes }) {
return [
"tr",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0
]
}
})

View File

@@ -0,0 +1,55 @@
const icons = {
colorPicker: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path fill="rgb(var(--color-text-300))" d="M20 14c-.092.064-2 2.083-2 3.5 0 1.494.949 2.448 2 2.5.906.044 2-.891 2-2.5 0-1.5-1.908-3.436-2-3.5zM9.586 20c.378.378.88.586 1.414.586s1.036-.208 1.414-.586l7-7-.707-.707L11 4.586 8.707 2.293 7.293 3.707 9.586 6 4 11.586c-.378.378-.586.88-.586 1.414s.208 1.036.586 1.414L9.586 20zM11 7.414 16.586 13H5.414L11 7.414z"></path></svg>`,
deleteColumn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z"/></svg>`,
deleteRow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M20 5c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1 .628.835 1 1.874 1 3 0 2.761-2.239 5-5 5s-5-2.239-5-5c0-1.126.372-2.165 1-3H4c-.552 0-1-.448-1-1V6c0-.552.448-1 1-1h16zm-7 10v2h6v-2h-6zm6-8H5v4h14V7z"/></svg>`,
insertLeftTableIcon: `<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
>
<path
d="M224.617-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm147.691-607.69q0-4.616-3.846-8.463-3.846-3.846-8.462-3.846H600q-4.616 0-8.462 3.846-3.847 3.847-3.847 8.463v535.382q0 4.616 3.847 8.463Q595.384-200 600-200h135.383q4.616 0 8.462-3.846 3.846-3.847 3.846-8.463v-535.382ZM587.691-200h160-160Z"
fill="rgb(var(--color-text-300))"
/>
</svg>
`,
insertRightTableIcon: `<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
>
<path
d="M600-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm-375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm-12.308-607.69v535.382q0 4.616 3.846 8.463 3.846 3.846 8.462 3.846H360q4.616 0 8.462-3.846 3.847-3.847 3.847-8.463v-535.382q0-4.616-3.847-8.463Q364.616-760 360-760H224.617q-4.616 0-8.462 3.846-3.846 3.847-3.846 8.463Zm160 547.691h-160 160Z"
fill="rgb(var(--color-text-300))"
/>
</svg>
`,
insertTopTableIcon: `<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
>
<path
d="M212.309-527.693q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0 375.383q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-59.999h535.382q4.616 0 8.463-3.846 3.846-3.846 3.846-8.462V-360q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H212.309q-4.616 0-8.463 3.847Q200-364.616 200-360v135.383q0 4.616 3.846 8.462 3.847 3.846 8.463 3.846Zm-12.309-160v160-160Z"
fill="rgb(var(--color-text-300))"
/>
</svg>
`,
insertBottomTableIcon:`<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
>
<path
d="M212.309-152.31q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-375.383q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm535.382-219.998H212.309q-4.616 0-8.463 3.846-3.846 3.846-3.846 8.462V-600q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h535.382q4.616 0 8.463-3.847Q760-595.384 760-600v-135.383q0-4.616-3.846-8.462-3.847-3.846-8.463-3.846ZM200-587.691v-160 160Z"
fill="rgb(var(--color-text-300))"
/>
</svg>
`,
};
export default icons;

View File

@@ -0,0 +1 @@
export { default as default } from "./table"

View File

@@ -0,0 +1,117 @@
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
import { findParentNode } from "@tiptap/core";
import { DecorationSet, Decoration } from "@tiptap/pm/view";
const key = new PluginKey("tableControls");
export function tableControls() {
return new Plugin({
key,
state: {
init() {
return new TableControlsState();
},
apply(tr, prev) {
return prev.apply(tr);
},
},
props: {
handleDOMEvents: {
mousemove: (view, event) => {
const pluginState = key.getState(view.state);
if (
!(event.target as HTMLElement).closest(".tableWrapper") &&
pluginState.values.hoveredTable
) {
return view.dispatch(
view.state.tr.setMeta(key, {
setHoveredTable: null,
setHoveredCell: null,
}),
);
}
const pos = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (!pos) return;
const table = findParentNode((node) => node.type.name === "table")(
TextSelection.create(view.state.doc, pos.pos),
);
const cell = findParentNode(
(node) =>
node.type.name === "tableCell" ||
node.type.name === "tableHeader",
)(TextSelection.create(view.state.doc, pos.pos));
if (!table || !cell) return;
if (pluginState.values.hoveredCell?.pos !== cell.pos) {
return view.dispatch(
view.state.tr.setMeta(key, {
setHoveredTable: table,
setHoveredCell: cell,
}),
);
}
},
},
decorations: (state) => {
const pluginState = key.getState(state);
if (!pluginState) {
return null;
}
const { hoveredTable, hoveredCell } = pluginState.values;
const docSize = state.doc.content.size;
if (hoveredTable && hoveredCell && hoveredTable.pos < docSize && hoveredCell.pos < docSize) {
const decorations = [
Decoration.node(
hoveredTable.pos,
hoveredTable.pos + hoveredTable.node.nodeSize,
{},
{
hoveredTable,
hoveredCell,
},
),
];
return DecorationSet.create(state.doc, decorations);
}
return null;
},
},
});
}
class TableControlsState {
values;
constructor(props = {}) {
this.values = {
hoveredTable: null,
hoveredCell: null,
...props,
};
}
apply(tr: any) {
const actions = tr.getMeta(key);
if (actions?.setHoveredTable !== undefined) {
this.values.hoveredTable = actions.setHoveredTable;
}
if (actions?.setHoveredCell !== undefined) {
this.values.hoveredCell = actions.setHoveredCell;
}
return this;
}
}

View File

@@ -0,0 +1,530 @@
import { h } from "jsx-dom-cjs";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { Decoration, NodeView } from "@tiptap/pm/view";
import tippy, { Instance, Props } from "tippy.js";
import { Editor } from "@tiptap/core";
import {
CellSelection,
TableMap,
updateColumnsOnResize,
} from "@tiptap/prosemirror-tables";
import icons from "./icons";
export function updateColumns(
node: ProseMirrorNode,
colgroup: HTMLElement,
table: HTMLElement,
cellMinWidth: number,
overrideCol?: number,
overrideValue?: any,
) {
let totalWidth = 0;
let fixedWidth = true;
let nextDOM = colgroup.firstChild as HTMLElement;
const row = node.firstChild;
if (!row) return;
for (let i = 0, col = 0; i < row.childCount; i += 1) {
const { colspan, colwidth } = row.child(i).attrs;
for (let j = 0; j < colspan; j += 1, col += 1) {
const hasWidth =
overrideCol === col ? overrideValue : colwidth && colwidth[j];
const cssWidth = hasWidth ? `${hasWidth}px` : "";
totalWidth += hasWidth || cellMinWidth;
if (!hasWidth) {
fixedWidth = false;
}
if (!nextDOM) {
colgroup.appendChild(document.createElement("col")).style.width =
cssWidth;
} else {
if (nextDOM.style.width !== cssWidth) {
nextDOM.style.width = cssWidth;
}
nextDOM = nextDOM.nextSibling as HTMLElement;
}
}
}
while (nextDOM) {
const after = nextDOM.nextSibling;
nextDOM.parentNode?.removeChild(nextDOM);
nextDOM = after as HTMLElement;
}
if (fixedWidth) {
table.style.width = `${totalWidth}px`;
table.style.minWidth = "";
} else {
table.style.width = "";
table.style.minWidth = `${totalWidth}px`;
}
}
const defaultTippyOptions: Partial<Props> = {
allowHTML: true,
arrow: false,
trigger: "click",
animation: "scale-subtle",
theme: "light-border no-padding",
interactive: true,
hideOnClick: true,
placement: "right",
};
function setCellsBackgroundColor(editor: Editor, backgroundColor) {
return editor
.chain()
.focus()
.updateAttributes("tableCell", {
background: backgroundColor,
})
.updateAttributes("tableHeader", {
background: backgroundColor,
})
.run();
}
const columnsToolboxItems = [
{
label: "Add Column Before",
icon: icons.insertLeftTableIcon,
action: ({ editor }: { editor: Editor }) =>
editor.chain().focus().addColumnBefore().run(),
},
{
label: "Add Column After",
icon: icons.insertRightTableIcon,
action: ({ editor }: { editor: Editor }) =>
editor.chain().focus().addColumnAfter().run(),
},
{
label: "Pick Column Color",
icon: icons.colorPicker,
action: ({
editor,
triggerButton,
controlsContainer,
}: {
editor: Editor;
triggerButton: HTMLElement;
controlsContainer;
}) => {
createColorPickerToolbox({
triggerButton,
tippyOptions: {
appendTo: controlsContainer,
},
onSelectColor: (color) => setCellsBackgroundColor(editor, color),
});
},
},
{
label: "Delete Column",
icon: icons.deleteColumn,
action: ({ editor }: { editor: Editor }) =>
editor.chain().focus().deleteColumn().run(),
},
];
const rowsToolboxItems = [
{
label: "Add Row Above",
icon: icons.insertTopTableIcon,
action: ({ editor }: { editor: Editor }) =>
editor.chain().focus().addRowBefore().run(),
},
{
label: "Add Row Below",
icon: icons.insertBottomTableIcon,
action: ({ editor }: { editor: Editor }) =>
editor.chain().focus().addRowAfter().run(),
},
{
label: "Pick Row Color",
icon: icons.colorPicker,
action: ({
editor,
triggerButton,
controlsContainer,
}: {
editor: Editor;
triggerButton: HTMLButtonElement;
controlsContainer:
| Element
| "parent"
| ((ref: Element) => Element)
| undefined;
}) => {
createColorPickerToolbox({
triggerButton,
tippyOptions: {
appendTo: controlsContainer,
},
onSelectColor: (color) => setCellsBackgroundColor(editor, color),
});
},
},
{
label: "Delete Row",
icon: icons.deleteRow,
action: ({ editor }: { editor: Editor }) =>
editor.chain().focus().deleteRow().run(),
},
];
function createToolbox({
triggerButton,
items,
tippyOptions,
onClickItem,
}: {
triggerButton: HTMLElement;
items: { icon: string; label: string }[];
tippyOptions: any;
onClickItem: any;
}): Instance<Props> {
const toolbox = tippy(triggerButton, {
content: h(
"div",
{ className: "tableToolbox" },
items.map((item) =>
h(
"div",
{
className: "toolboxItem",
onClick() {
onClickItem(item);
},
},
[
h("div", {
className: "iconContainer",
innerHTML: item.icon,
}),
h("div", { className: "label" }, item.label),
],
),
),
),
...tippyOptions,
});
return Array.isArray(toolbox) ? toolbox[0] : toolbox;
}
function createColorPickerToolbox({
triggerButton,
tippyOptions,
onSelectColor = () => {},
}: {
triggerButton: HTMLElement;
tippyOptions: Partial<Props>;
onSelectColor?: (color: string) => void;
}) {
const items = {
Default: "rgb(var(--color-primary-100))",
Orange: "#FFE5D1",
Grey: "#F1F1F1",
Yellow: "#FEF3C7",
Green: "#DCFCE7",
Red: "#FFDDDD",
Blue: "#D9E4FF",
Pink: "#FFE8FA",
Purple: "#E8DAFB",
};
const colorPicker = tippy(triggerButton, {
...defaultTippyOptions,
content: h(
"div",
{ className: "tableColorPickerToolbox" },
Object.entries(items).map(([key, value]) =>
h(
"div",
{
className: "toolboxItem",
onClick: () => {
onSelectColor(value);
colorPicker.hide();
},
},
[
h("div", {
className: "colorContainer",
style: {
backgroundColor: value,
},
}),
h(
"div",
{
className: "label",
},
key,
),
],
),
),
),
onHidden: (instance) => {
instance.destroy();
},
showOnCreate: true,
...tippyOptions,
});
return colorPicker;
}
export class TableView implements NodeView {
node: ProseMirrorNode;
cellMinWidth: number;
decorations: Decoration[];
editor: Editor;
getPos: () => number;
hoveredCell;
map: TableMap;
root: HTMLElement;
table: HTMLElement;
colgroup: HTMLElement;
tbody: HTMLElement;
rowsControl?: HTMLElement;
columnsControl?: HTMLElement;
columnsToolbox?: Instance<Props>;
rowsToolbox?: Instance<Props>;
controls?: HTMLElement;
get dom() {
return this.root;
}
get contentDOM() {
return this.tbody;
}
constructor(
node: ProseMirrorNode,
cellMinWidth: number,
decorations: Decoration[],
editor: Editor,
getPos: () => number,
) {
this.node = node;
this.cellMinWidth = cellMinWidth;
this.decorations = decorations;
this.editor = editor;
this.getPos = getPos;
this.hoveredCell = null;
this.map = TableMap.get(node);
if (editor.isEditable) {
this.rowsControl = h(
"div",
{ className: "rowsControl" },
h("button", {
onClick: () => this.selectRow(),
}),
);
this.columnsControl = h(
"div",
{ className: "columnsControl" },
h("button", {
onClick: () => this.selectColumn(),
}),
);
this.controls = h(
"div",
{ className: "tableControls", contentEditable: "false" },
this.rowsControl,
this.columnsControl,
);
this.columnsToolbox = createToolbox({
triggerButton: this.columnsControl.querySelector("button"),
items: columnsToolboxItems,
tippyOptions: {
...defaultTippyOptions,
appendTo: this.controls,
},
onClickItem: (item) => {
item.action({
editor: this.editor,
triggerButton: this.columnsControl?.firstElementChild,
controlsContainer: this.controls,
});
this.columnsToolbox?.hide();
},
});
this.rowsToolbox = createToolbox({
triggerButton: this.rowsControl.firstElementChild,
items: rowsToolboxItems,
tippyOptions: {
...defaultTippyOptions,
appendTo: this.controls,
},
onClickItem: (item) => {
item.action({
editor: this.editor,
triggerButton: this.rowsControl?.firstElementChild,
controlsContainer: this.controls,
});
this.rowsToolbox?.hide();
},
});
}
// Table
this.colgroup = h(
"colgroup",
null,
Array.from({ length: this.map.width }, () => 1).map(() => h("col")),
);
this.tbody = h("tbody");
this.table = h("table", null, this.colgroup, this.tbody);
this.root = h(
"div",
{
className: "tableWrapper controls--disabled",
},
this.controls,
this.table,
);
this.render();
}
update(node: ProseMirrorNode, decorations) {
if (node.type !== this.node.type) {
return false;
}
this.node = node;
this.decorations = decorations;
this.map = TableMap.get(this.node);
if (this.editor.isEditable) {
this.updateControls();
}
this.render();
return true;
}
render() {
if (this.colgroup.children.length !== this.map.width) {
const cols = Array.from({ length: this.map.width }, () => 1).map(() =>
h("col"),
);
this.colgroup.replaceChildren(...cols);
}
updateColumnsOnResize(
this.node,
this.colgroup,
this.table,
this.cellMinWidth,
);
}
ignoreMutation() {
return true;
}
updateControls() {
const { hoveredTable: table, hoveredCell: cell } = Object.values(
this.decorations,
).reduce(
(acc, curr) => {
if (curr.spec.hoveredCell !== undefined) {
acc["hoveredCell"] = curr.spec.hoveredCell;
}
if (curr.spec.hoveredTable !== undefined) {
acc["hoveredTable"] = curr.spec.hoveredTable;
}
return acc;
},
{} as Record<string, HTMLElement>,
) as any;
if (table === undefined || cell === undefined) {
return this.root.classList.add("controls--disabled");
}
this.root.classList.remove("controls--disabled");
this.hoveredCell = cell;
const cellDom = this.editor.view.nodeDOM(cell.pos) as HTMLElement;
const tableRect = this.table.getBoundingClientRect();
const cellRect = cellDom.getBoundingClientRect();
this.columnsControl.style.left = `${
cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft
}px`;
this.columnsControl.style.width = `${cellRect.width}px`;
this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`;
this.rowsControl.style.height = `${cellRect.height}px`;
}
selectColumn() {
if (!this.hoveredCell) return;
const colIndex = this.map.colCount(
this.hoveredCell.pos - (this.getPos() + 1),
);
const anchorCellPos = this.hoveredCell.pos;
const headCellPos =
this.map.map[colIndex + this.map.width * (this.map.height - 1)] +
(this.getPos() + 1);
const cellSelection = CellSelection.create(
this.editor.view.state.doc,
anchorCellPos,
headCellPos,
);
this.editor.view.dispatch(
// @ts-ignore
this.editor.state.tr.setSelection(cellSelection),
);
}
selectRow() {
if (!this.hoveredCell) return;
const anchorCellPos = this.hoveredCell.pos;
const anchorCellIndex = this.map.map.indexOf(
anchorCellPos - (this.getPos() + 1),
);
const headCellPos =
this.map.map[anchorCellIndex + (this.map.width - 1)] +
(this.getPos() + 1);
const cellSelection = CellSelection.create(
this.editor.state.doc,
anchorCellPos,
headCellPos,
);
this.editor.view.dispatch(
// @ts-ignore
this.editor.view.state.tr.setSelection(cellSelection),
);
}
}

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