Compare commits

..

97 Commits

Author SHA1 Message Date
pablohashescobar e5e1619c98 dev: fix space authentication 2024-05-10 18:10:11 +05:30
M. Palanikannan 0ad8bf7664 [WEB-1118] fix: table selections using drag handle fixed (#4429)
* fix: table selections in using drag handle fixed

* fix: not show drag handles for empty p tags
2024-05-10 17:32:23 +05:30
Nikhil 88ebda42ff fix: authentication redirection and UI (#4432)
* dev: update python version

* dev: handle magic code attempt exhausted

* dev: update app, space and god mode redirection paths

* fix: handled signup and signin workflow

* chore: auth input error indication and autofill styling improvement

* dev: add app redirection urls

* dev: update redirections

* chore: onboarding improvement

* chore: onboarding improvement

* chore: redirection issue in space resolved

* chore: instance empty state added

* dev: fix app, space, admin redirection in docker setitngs

---------

Co-authored-by: guru_sainath <gurusainath007@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-05-10 17:30:38 +05:30
M. Palanikannan 2d1201cc92 fix: button click propagation stopped (#4430) 2024-05-10 16:41:13 +05:30
rahulramesha b725c69882 list and spreadsheet sub issues mutation issue (#4415) 2024-05-10 16:14:15 +05:30
Manish Gupta 74eb50aa1a selfhosting fixes for custom branch and platform (#4431) 2024-05-10 16:08:04 +05:30
Anmol Singh Bhatia dc77e4afdb chore: project publish modal improvement (#4422) 2024-05-10 15:25:16 +05:30
sriram veeraghanta 27315a8aa2 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-05-10 15:24:32 +05:30
Anmol Singh Bhatia da78933c61 [WEB-1274] chore: issue spreadsheet enhancement (#4423)
* chore: border and background remove from cycle and module select

* choe: indentation improvement
2024-05-10 15:24:18 +05:30
sriram veeraghanta 0ab2a98777 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-05-10 15:24:13 +05:30
sriram veeraghanta 40560109b5 fix: admin app redirections 2024-05-10 15:23:51 +05:30
Anmol Singh Bhatia 0037377c8f chore: project card enhancement (#4424) 2024-05-10 15:22:01 +05:30
Anmol Singh Bhatia 0af55e7bbb [WEB-1250] chore: module list enhancement (#4425)
* chore: move module sub-header to app header

* chore: gantt header improvement, remove title

* chore: progress indicator size reduced

* chore: replace members with lead and updated start and end date ui
2024-05-10 15:21:05 +05:30
Anmol Singh Bhatia 57eda34082 chore: notification action item enhancement (#4426) 2024-05-10 15:19:59 +05:30
Anmol Singh Bhatia e396424db7 [WEB-1251] chore: view list enhancement (#4427)
* chore: moved search query to mobx store

* chore: moved view sub-header to app header

* chore: created by avatar added in view item list
2024-05-10 15:19:05 +05:30
sriram veeraghanta 243680132e fix: space re-directions 2024-05-10 03:46:45 +05:30
sriram veeraghanta 547a76ae55 fix: admin and space redirections (#4419)
* dev: add admin and space base url

* fix: formatting

* dev: add app,space and admin base url to the api env

* fix: updated app base urls redirection

* dev: add change password endpoint

* dev: add none as default for base url

* dev: space password management endpoints

* fix: docker env update

* fix: docker and env settings

* fix: docker changes

* fix: next config update

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: guru_sainath <gurusainath007@gmail.com>
2024-05-10 02:32:42 +05:30
sriram veeraghanta 2320b33189 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-05-09 21:06:08 +05:30
sriram veeraghanta 45bb1153ee fix: removing deploy with nginx env 2024-05-09 21:05:51 +05:30
Anmol Singh Bhatia b8768d7274 fix: spreadsheet layout sticky column (#4416)
* fix: spreadsheet layout sticky column

* fix: spreadsheet layout sticky column
2024-05-09 19:06:39 +05:30
guru_sainath 58bf056ddb fix: auth redirection issues in the web, space and admin apps (#4414)
* fix: login redirection

* dev: log the user out when deactivating the account

* dev: update redirect uris for google and github

* fix: redirection url and invitation api and add redirection to god mode in nginx

* dev: add reset password redirection

* dev: update nginx headers

* dev: fix setup sh and env example and put validation for use minio when fetching project covers

* dev: stabilize dev setup

* fix: handled redirection error in web, space, and admin apps

* fix: resovled build errors

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-05-09 17:46:31 +05:30
rahulramesha 692f570258 [WEB-1134] fix: module link mutation issue (#4413)
* fix module link mutation issue

* reference moduleDetails?.link_module inside a prop of Disclosure instead of in a div
2024-05-09 16:49:24 +05:30
Anmol Singh Bhatia 11cd9f57d7 chore: applied filters update view button alignment improvement (#4407) 2024-05-09 15:52:56 +05:30
Aaryan Khandelwal 3a66986785 [WEB-1256] fix: accept image as a valid comment (#4409)
* fix: accept image as a valid comment

* fix: update space app helper function
2024-05-09 15:52:17 +05:30
rahulramesha d8ab3e0087 [WEB-1139] chore: Calendar pragmatic dnd (#4410)
* replace Pragmatic DND for calendar

* remove unnecessary check
2024-05-09 15:51:49 +05:30
sriram veeraghanta 9d2d9c59ca fix: workflow updates 2024-05-09 02:52:00 +05:30
sriram veeraghanta cfdcda4445 fix: build test workflow update 2024-05-09 01:41:48 +05:30
sriram veeraghanta 230fd961c7 feat: adding admin service to workflow 2024-05-09 00:56:18 +05:30
sriram veeraghanta 59335618b4 feat: session auth implementation (#4411)
* feat: session authentication and god-mode implementation (#4302)

* dev: move authentication to base class for credentials

* chore: new account creation

* dev: return error as query parameter

* dev: accounts and profile endpoints for user

* fix: user store updates

* fix: store fixes

* fix: type fixes

* dev: set is_password_autoset and is_email_verifier for auth providers

* dev: move all auth configuration to different apps

* dev: fix circular imports

* dev: remove unused imports

* dev: fix imports for authentication

* dev: update endpoints to use rest framework api viewa

* fix: onboarding fixes

* dev: session model changes

* fix: session model and add check for last name first name and avatar

* dev: fix referer redirect

* dev: remove auth imports

* dev: fix imports

* dev: update migrations

* fix: instance admin login

* comflict: conflicts resolved

* dev: fix import errors and email check endpoint

* fix: error messages and redirects after login

* dev: configs api

* fix: is github enabled boolean

* dev: merge config and instance api

* conflict: merge conflict resolved

* dev: instance admin sign up endpoint

* dev: enable magic link login

* dev: configure instance variables for github and google enabled

* chore: typo fixes

* fix: god mode docker file changes

* build-error: resolved build errors

* fix: docker compose changes

* dev: add email credential check endpoint

* fix: minor package changes

* fix: docker related changes

* dev: add nginx rules in the nginx template

* dev: refactor the url patterns

* fix: docker changes

* fix: docker files for god-mode

* fix: static export

* fix: nginx conf

* dev: smtp sender refused exception

* fix: godmode fixes

* chore: god mode revamp.

* dev: add csrf secured flag

* fix: oauth redirect uri and session settings

* chore: god mode app changes.  (#3982)

* chore: send test email functionality.

* style: authentication methods page UI revamp.

* chore: create workspace popup.

* fix: user me endpoint

* dev: fix redirection after authentication

* dev: handle god mode redirection

* fix: redirections

* fix: auth related hooks

* fix: store related fixes

* dev: fix session authentication for rest apis

* fix: linting errors

* fix: removing references of useStore=

* dev: fix redirection and password validation

* dev: add useUser hook

* fix: build fixes and lint issues

* fix: removing useApplication hook

* fix: build errors

* fix: delete unused files

* fix: auth build fixes

* fix: bugfixes

* dev: alter avatar to support more than 255 chars

* dev: fix profile endpoint and increase session expiry time and update session on every request

* chore: resolved the migration

* chore: resolved merge conflicts

* dev: error codes and error messages for the auth flow

* dev: instance admin sign up and sign in endpoint

* dev: use zxcvbn to validate password strength

* dev: add extra parameters when error handling on instance god mode

* chore: auth init

* chore: signin/ signup form ui updates and password strength meter.

* chore: update password fields.

* chore: validations and error handling.

* chore: updated sign-up form

* chore: updated workflow and updated the code structure

* chore: instance empty state for god-mode.

* chore: instance and auth wrappers update

* fix: renaming godmode

* fix: docker changes

* chore: updated authentication wrappers

* chore: updated the authentication workflow and rendered all pages

* fix: build errors

* fix: docker related fixes

* fix: tailing slash added to space and admin for valid nginx locations

* chore: seperate pages for signup and login

* git-action modified for admin file changes

* feature build action updated for admin app

* self host modified

* chore: resolved build errors and handled signin and signup in a seperate route

* chore: sign-in and sign-up revamp.

* fix: migration conflicts

* dev: migrations

* chore: handled redirection

* dev: admin url

* dev: create seperate endpoint for instance admin me

* dev: instance admin endpoint

* git action fixed

* chore: handled auth wrappers

* dev: add serializer and remove print logs

* fix: build errors

* dev: fix migrations

* dev: instance folder structuring

* fix: linting errors

* chore: resolved build errors

* chore: updated store and auth workflow and updates api service types

* chore: Replaced Next Link with Anchoer tag for god-mode redirection

* add 3333 port to allowed origins

* make password login working again

* dev: fix redirection, add admin signout endpoint and fix email credential check endpoint

* fix unique code sign in

* fix small build error

* enable sign out

* dev: add google client secret variable to configure instance

* dev: add referer for redirection

* fix origin urls for oauths

* admin setup and login separation

* dev: fix user redirection and tour completed endpoint

* fix build errors

* dev: add set password endpoint

* dev: remove user creation logic for redirection

* fix unique code page

* fix forgot password

* chore: onboarding revamp.

* dev: fix workspace slug redirection in login

* chore: invited user onboarding flow update.

* chore: fix switch or delete account modal.

* fix members exception

* refactor auth flows and add invitations to auth flow

* fix sig in sign up url

* fix action url

* fix build errors

* dev: fix user set password when logging in

* dev: reset password endpoint

* chore: confirm password validation for signup and onboarding.

* enable reset password

* fix build error

* chore: minor UI updates.

* chore: forgot and reset password UI revamp.

* fix authentication re directions

* dev: auth redirections

* change url paths for signup and signin

* dev: make the user logged in when changing passwords

* dev: next path redirection for web and space app

* dev: next path for magic sign in endpoint

* dev: github space endpoint

* chore: minor ui updates and fixes in web app.

* set password screen

* fix multiple unique code generation

* dev: next path base redirection

* dev: remove print logs

* dev: auth space endpoints

* fix build errors

* dev: invalidate cache on configuration update, god mode exception errors and authentication failed code

* dev: fix space endpoints and add extra endpoints

* chore: space auth revamp.

* dev: add sign up for space app

* fix: build errors.

* fix: auth redirection logic.

* chore: space app onboarding revamp.

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Manish Gupta <manish@mgupta.me>
Co-authored-by: = <=>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>

* chore: updated file structure for admin

* chore: updated admin-sidebar

* chore: auth error handling

* chore: onboarding UI updates and dark mode fixes.

* chore: add `user personalization` step to onboarding profile setup screen.

* chore: fix minor UI bugs

* chore: authentication workflow changes

* chore: handled signin workflow

* style: switch or delete account workflow

* chore: god mode redirection URL

* feat(dashboard): improve label readability (#4321)

change none label for all time in dashbard filters

* chore: god-mode redirection

* chore: onboarding ui updates and accept invitation workflow updates.

* chore: rename unique code auth form.

* style: space auth ux copy.

* chore: updated intance and auth wrapper logic

* chore: update default layout style.

* chore: update confirm password.

* chore: backend redirection

* style: update banner ui

* chore: minor ui updates and validation fix.

* chore: removed old auth hook

* chore: handled auth wrapper

* chore: handled store loaders in the user

* chore: handled logs

* chore: add loading spinners for all auth and onboarding form buttons.

* chore: add background pattern in admin auth forms and minor ui fixes.

* chore: UI changes and revamp components for authentication

* chore: auth UI consistency in web, space and admin.

* chore: resolved build errors

* chore: removed old auth hooks

* chore: handled lint errors in use accounts

* chore: updated authentication wrapper logic in web app

* [WEB -1149] dev: update dependencies (#4333)

* dev: upgrade dependencies remove unwanted dependency and add ruff as local dependency

* dev: add comments

* chore: authentication wrapper fetch user

* chore: updated store loader

* chore: removed old auth wrapper and replaced the imports with new auth wrapper

* chore: join workspace invitation workflow updates

* chore: build error resolved in deploy

* chore: handled onboarding step error in web app

* chore: SMTP Name and Password validation removed

* chore: handled seo and signout logic and new user popup

* chore: added redirection to plane in the sidebar

* chore: resolved build errors

* dev: admin session cookie update

* chore: updated cookie session time for admin

* dev: add start date and end date to projects (#4355)

* chore: add email security dropdown and remove SMTP username and password validation.

* chore: add tooltip to admin sidebar help-section.

* chore: add dropdown to collapsed admin sidebar.

* chore: profile themning

* chore: updated page error messages and theme in command palette

* dev: add email validation in email check apis

* dev: remove start date and end date from project

* chore: updated space folder structure and updated the store hooks

* dev: error codes for authentication

* chore: handled authentication in space and web apps

* chore: banner redirect handling the email

* dev: god mode error codes

* chore: updated error codes

* chore: updated onboarding images

* dev: signout endpoints and saving login domain while creating sessions

* feat: Self Host Data Backup (#4383)

* feat: implemented backup , support for docker-compose tool, readme updated

* minor fix in shell script

* codacy fixes

* chore: handled build errors in web

* chore: updated react, react-dom, and next versions

* chore: updated password autioset in the signin

* dev: add logo prop to views and pages

* chore: updated api service and handled the set password in store

* chore: handled build errors and code cleanup

* dev: return 401 when the session is not valid

* dev: users/me exception for api

* chore: installed lodash in space app

* dev: add auth route in nginx

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Manish Gupta <manish@mgupta.me>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Daniel Alba <56451942+redrum15@users.noreply.github.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
2024-05-08 23:01:20 +05:30
sriram veeraghanta ae43d05714 fix: upgrading posthog 2024-05-08 19:27:48 +05:30
Anmol Singh Bhatia e604cfeec9 [WEB-1040] fix: list layout indentation fix (#4406) 2024-05-08 17:28:28 +05:30
Aaryan Khandelwal 32e601a8e6 chore: remove description trim logic (#4396) 2024-05-08 15:42:52 +05:30
Bavisetti Narayan 50e3174ea8 chore: email templates (#4392) 2024-05-08 15:41:41 +05:30
Aaryan Khandelwal f2749f693f [WEB-1211] fix: inbox issue description revalidate (#4388) 2024-05-08 14:47:18 +05:30
Aaryan Khandelwal 3b17dc51ba [WEB-1208] fix: todo list item word break (#4405) 2024-05-08 14:44:18 +05:30
Anmol Singh Bhatia 2aaf0a1637 [WEB-1040] chore: list layout sub issue nesting implementation (#4404) 2024-05-08 14:39:27 +05:30
sriram veeraghanta f09dd3d782 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-05-08 14:24:48 +05:30
sriram veeraghanta 2cbb2db02e fix: community edition label 2024-05-08 14:24:19 +05:30
Aaryan Khandelwal f7babd682e dev: upgrade lucide-react version to the latest one (#4397) 2024-05-08 14:01:05 +05:30
Anmol Singh Bhatia a46c507ca1 [WEB-1040] chore: spreadsheet indentation improvement (#4391)
* chore: spreadsheet indentation improvement

* chore: spreadsheet layout sub issu nesting improvement

* chore: sub issue spacing improvement

* chore: spreadsheet layout sub issue toggle button improvement
2024-05-08 13:59:51 +05:30
Prateek Shourya 10efd8d1d9 [WEB-1236] chore: add Create page button to public/ private page empty state. (#4401) 2024-05-08 13:43:08 +05:30
Prateek Shourya 6efa135e9e [WEB-1217] style: fix inconsistency in height of project type and lead dropdown in create project modal. (#4400) 2024-05-08 13:42:40 +05:30
Aaryan Khandelwal 6793b9e6fa [WEB-] chore: show code block copy button only on hover (#4361) 2024-05-08 13:42:11 +05:30
Prateek Shourya 760063ec75 [WEB-1182] style: fix inbox state filter title. (#4399) 2024-05-08 13:41:58 +05:30
Prateek Shourya 1cbbddb1be [WEB-1215] chore: display favorite module filter in applied filter section. (#4402)
* [WEB-1215] chore: display `favorite module` filter in applied filter section.

* fix: build errors.
2024-05-08 13:40:58 +05:30
Aaryan Khandelwal cc4bb385fe [WEB-] chore: trigger command k from input fields and editors (#4362) 2024-05-08 13:40:25 +05:30
rahulramesha 13e6a67321 [WEB-1140] chore: Gantt pragmatic dnd (#4390)
* Gantt Drag and drop migration and enable Dnd in Modules and Cycles Gantt

* fix minor UI and code issues
2024-05-08 13:38:58 +05:30
Bavisetti Narayan b8f1734738 fix: draft archive issues (#4393) 2024-05-07 20:31:44 +05:30
Bavisetti Narayan d946c51ef1 chore: created by key changed (#4389) 2024-05-07 19:22:00 +05:30
Nikhil a40517015b [WEB - 1213] fix: module webhook (#4385)
* dev: fix module webhook

* dev: correct the comment

* dev: handle does not exist exception
2024-05-07 15:51:08 +05:30
Aaryan Khandelwal 967ad77078 fix: set focus on editor conditionally after image upload (#4387) 2024-05-07 15:49:11 +05:30
Prateek Shourya f5f683b8b8 [WEB-1086] chore: update toast messages for consistency. (#4384) 2024-05-07 15:03:24 +05:30
Anmol Singh Bhatia 2aef40b7c5 [WEB-1176] chore: filter dropdown indicator and code refactor (#4379)
* chore: filter dropdown indicator and code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: refactor calculateTotalFilters function with typescript generics
2024-05-07 14:56:19 +05:30
Anmol Singh Bhatia 1eba6c24cd [WEB-859] chore: inbox sidebar improvement (#4381)
* chore: added created by field in inbox issue

* chore: inbox sidebar list item created by avatar added

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-05-07 14:10:27 +05:30
Prateek Shourya a46eccf300 [WEB-1117] chore: add tool tip for Snooze under Notification Panel. (#4377) 2024-05-07 14:08:02 +05:30
Aaryan Khandelwal 780b239ecb [WEB-1142] chore: optimistically add issue to cycle/modules (#4334)
* chore: optimistically add issue to cycle and module

* chore: update toast alerts

* refactor: module issue store

* chore: added addCycleToIssueFunction
2024-05-07 14:05:56 +05:30
Anmol Singh Bhatia a85517de99 [WEB-1098] chore: dropdowns enhancement (#4375) 2024-05-07 14:02:48 +05:30
P B fb68b77068 Changed tagline, description, and disclaimer (#4382)
- Minor grammatical and language corrections
- Another update coming soon
2024-05-07 13:56:03 +05:30
Aaryan Khandelwal 20e7dc68e6 [WEB-1127] style: create and delete modals' consistency (#4345)
* style: update modals typography, alignment

* style: made the modal separator full width

* style: delete modals consistency

* style: update the remaining delete modals

* chore: delete modal secondary button text

* style: update the remaining create modals

* chore: update cancel button text

* chore: created modal core

* style: modals responsiveness
2024-05-07 12:44:36 +05:30
Anmol Singh Bhatia 5ef51edad7 [WEB-1134] fix: module link create and delete mutation (#4373)
* fix: module link create and delete mutation

* chore: module link mutation store updates

* chore: code refactor

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-05-06 17:56:35 +05:30
Bavisetti Narayan 06a664f6b9 [WEB-1151] fix: email notification for mentions (#4360)
* chore: mention notification

* chore: mention filters
2024-05-06 16:07:56 +05:30
Anmol Singh Bhatia cdab12e4b6 [WEB-1157] chore: label select improvement (#4370) 2024-05-06 15:30:58 +05:30
Anmol Singh Bhatia 59f1cc1962 [WEB-1114] chore: recent activity message updated (#4371) 2024-05-06 15:28:33 +05:30
Anmol Singh Bhatia 562e50815d [WEB-1007] chore: invalid issue error empty state added (#4372) 2024-05-06 15:27:56 +05:30
rahulramesha 463f4781ae [WEB-1137] fix: Firefox distorted vertical text (#4376) 2024-05-06 15:22:45 +05:30
Bavisetti Narayan 653005bb3b [WEB-1206] chore: bulk delete api logs (#4374)
* chore: bulk delete api logs

* chore: deletion time change
2024-05-06 15:19:03 +05:30
Nikhil f1fda4ae4a [WEB - 1122] fix: webhook for issues, issue comments, projects, cycles and modules. (#4330)
* dev: update webhook logic for issues

* dev: update issue webhooks for cycle and module

* dev: webhook for comment

* dev: issue attachment webhooks

* dev: add logging

* dev: add inbox issue webhooks

* dev: update the webhook send task

* dev: project webhooks for api

* dev: webhooks update for projects, cycles and modules

* dev: fix webhook on cycle and module create from external apis
2024-05-06 14:13:49 +05:30
Aaryan Khandelwal fb74875cde [WEB-1181] chore: added a loader for page description (#4358)
* chore: add loader for page description

* chore: added skeleton loader

* fix: title loader margin

* chore: increased laoder width
2024-05-03 23:12:44 +05:30
Anmol Singh Bhatia c96225c812 [WEB-1183] fix: updated global issues filter while updating global view (#4357)
* chore: Updated global issues filter while updating global view

* fix: globale view modal clear all

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-05-03 20:29:11 +05:30
Aaryan Khandelwal 527ecd7d22 style: make edito height according to the content height (#4347) 2024-05-03 19:20:13 +05:30
rahulramesha acd8f8d2d6 [WEB-1173] fix: order by for last updated when issue is updated (#4353)
* update the issue's updated at date when issue is updated

* sort issue's updated and created at regardless of the date format.

* move the logic to date time helpers

* revert back the third variable in update issue
2024-05-03 19:08:20 +05:30
Anmol Singh Bhatia f4cc103238 [WEB-1175] fix: modal context (#4352)
* fix: modal context

* chore: code refactor
2024-05-03 17:58:15 +05:30
Aaryan Khandelwal 89f2f87b97 fix: decrease gantt full size z-index (#4354) 2024-05-03 17:38:21 +05:30
sriram veeraghanta eed047bfe3 chore: update version 2024-05-03 17:23:12 +05:30
Aaryan Khandelwal 8cf4260219 fix: dropdowns should close on tab key (#4351) 2024-05-03 15:39:14 +05:30
rahulramesha 1b55411919 [WEB-1136] chore: Kanban drag and drop improvements (#4350)
* Kanban DnD improvement

* minor fixes for kanban dnd improvement

* change scroll duration

* fix feedback on the UX

* add highlight before drop

* add toast message explain drag and drop is currently disabled

* Change warning dnd message

* add comments

* fix minor build error
2024-05-03 15:12:06 +05:30
Nikhil dc5edca34d chore: update the default name that is added to instance (#4348) 2024-05-03 14:35:36 +05:30
Anmol Singh Bhatia 2884b0e22e fix: inbox issue header quick action validation (#4344) 2024-05-03 14:17:21 +05:30
Aaryan Khandelwal 091fdb89ac chore: hide display properties in the calendar layout (#4346) 2024-05-03 14:16:27 +05:30
Anmol Singh Bhatia a678844073 [WEB-859] [WEB-1162] chore: issue detail improvement and assignees filter fix (#4342)
* chore: created by option added to inbox issue detail section

* chore: issue detail page improvement

* fix: inbox assignee filter key
2024-05-02 20:39:43 +05:30
Bavisetti Narayan 4cb5c4d9df chore: workspace module list endpoint (#4343) 2024-05-02 19:18:41 +05:30
Aaryan Khandelwal 45c9dfb3cf chore: update useCallback dependencies (#4341) 2024-05-02 19:18:08 +05:30
M. Palanikannan 4c78cd7c7e fix: inbox create issue comment fixed (#4340) 2024-05-02 18:22:00 +05:30
Aaryan Khandelwal c4229c9d55 fix: quick actions dropdown disabled state (#4335) 2024-05-02 16:13:58 +05:30
Aaryan Khandelwal 42c4c46939 [WEB-1154] fix: delete attachment modal logic (#4338)
* fix: delete attachment modal logic

* chore: remove console log

* chore: update delete attachment button type
2024-05-02 16:13:04 +05:30
Anmol Singh Bhatia 6918393b63 fix: existing issue list modal loading flicker (#4337) 2024-05-02 16:11:38 +05:30
Aaryan Khandelwal 6196c750f1 fix: issue description persistence (#4331) 2024-05-01 19:55:37 +05:30
Anmol Singh Bhatia ed6dd37043 fix: module sub-header empty state validation (#4329) 2024-05-01 18:36:11 +05:30
Bavisetti Narayan efa3eda85e [WEB-1145] chore: updated sub issue count in cycles, modules and project (#4328)
* chore: total issue count

* chore: removed the migration file

* fix: issue count

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-05-01 18:24:54 +05:30
Aaryan Khandelwal eb0877a3c8 [WEB-1135] chore: store page full width information in local storage (#4327)
* chore: store page full width information in local storage

* chore: update page types
2024-05-01 18:10:39 +05:30
Aaryan Khandelwal 73fd6e641c chore: show display name character in avatar (#4326) 2024-05-01 18:05:24 +05:30
Aaryan Khandelwal 34dd19cb00 chore: default pages sorting order (#4325) 2024-05-01 18:04:41 +05:30
Anmol Singh Bhatia ecc277c571 [WEB-1101] chore: workspace view quick action enhancement (#4324)
* chore: workspace view quick action enhancement

* fix: issue quick action height
2024-05-01 18:03:13 +05:30
Bavisetti Narayan d69f025b9a [WEB-1132] fix: display datetime fields in user time zone format (#4323)
* fix: user timezone response

* chore: removed unused variables
2024-05-01 18:01:53 +05:30
Aaryan Khandelwal ed4a0518fc [WEB-1119] style: editor typography, borders and alignment throughout the platform (#4322)
* chore: new font sizes

* chore: update space app editor border

* chore: issue detials page x-padding

* chore: editor width
2024-05-01 18:01:30 +05:30
rahulramesha 2e2747c1f9 fix kanban collapsed vertical writing mode in firefox (#4320) 2024-05-01 12:21:26 +05:30
Daniel Alba aa09ec7cd4 feat(dashboard): improve label readability (#4321)
change none label for all time in dashbard filters
2024-05-01 00:10:52 +05:30
654 changed files with 17095 additions and 20420 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ module.exports = {
extends: ["custom"],
settings: {
next: {
rootDir: ["web/", "space/"],
rootDir: ["web/", "space/", "admin/"],
},
},
};
+15 -15
View File
@@ -22,11 +22,11 @@ jobs:
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed }}
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
build_apiserver: ${{ steps.changed_files.outputs.apiserver_any_changed }}
build_admin: ${{ steps.changed_files.outputs.admin_any_changed }}
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
build_backend: ${{ steps.changed_files.outputs.backend_any_changed }}
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
steps:
- id: set_env_variables
@@ -54,8 +54,12 @@ jobs:
uses: tj-actions/changed-files@v42
with:
files_yaml: |
frontend:
- web/**
apiserver:
- apiserver/**
proxy:
- nginx/**
admin:
- admin/**
- packages/**
- 'package.json'
- 'yarn.lock'
@@ -68,20 +72,16 @@ jobs:
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
admin:
- admin/**
web:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
backend:
- apiserver/**
proxy:
- nginx/**
branch_build_push_frontend:
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -236,8 +236,8 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_backend:
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
branch_build_push_apiserver:
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
+44 -11
View File
@@ -10,8 +10,9 @@ jobs:
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
admin_changed: ${{ steps.changed-files.outputs.admin_any_changed }}
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
space_changed: ${{ steps.changed-files.outputs.deploy_any_changed }}
steps:
- uses: actions/checkout@v3
- name: Get changed files
@@ -21,20 +22,27 @@ jobs:
files_yaml: |
apiserver:
- apiserver/**
web:
- web/**
admin:
- admin/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
deploy:
space:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
web:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
lint-apiserver:
needs: get-changed-files
@@ -45,7 +53,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x' # Specify the Python version you need
python-version: "3.x" # Specify the Python version you need
- name: Install Pylint
run: python -m pip install ruff
- name: Install Apiserver Dependencies
@@ -53,9 +61,9 @@ jobs:
- name: Lint apiserver
run: ruff check --fix apiserver
lint-web:
lint-admin:
needs: get-changed-files
if: needs.get-changed-files.outputs.web_changed == 'true'
if: needs.get-changed-files.outputs.admin_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -64,7 +72,7 @@ jobs:
with:
node-version: 18.x
- run: yarn install
- run: yarn lint --filter=web
- run: yarn lint --filter=admin
lint-space:
needs: get-changed-files
@@ -79,8 +87,9 @@ jobs:
- run: yarn install
- run: yarn lint --filter=space
build-web:
needs: lint-web
lint-web:
needs: get-changed-files
if: needs.get-changed-files.outputs.web_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -89,7 +98,19 @@ jobs:
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=web
- run: yarn lint --filter=web
build-admin:
needs: lint-admin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=admin
build-space:
needs: lint-space
@@ -102,3 +123,15 @@ jobs:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=space
build-web:
needs: lint-web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=web
+17 -11
View File
@@ -5,17 +5,17 @@ on:
inputs:
web-build:
required: false
description: 'Build Web'
description: "Build Web"
type: boolean
default: true
space-build:
required: false
description: 'Build Space'
description: "Build Space"
type: boolean
default: false
admin-build:
required: false
description: 'Build Admin'
description: "Build Admin"
type: boolean
default: false
@@ -35,7 +35,7 @@ jobs:
echo "BUILD_SPACE=$BUILD_SPACE"
echo "BUILD_ADMIN=$BUILD_ADMIN"
outputs:
web-build: ${{ env.BUILD_WEB}}
web-build: ${{ env.BUILD_WEB}}
space-build: ${{env.BUILD_SPACE}}
admin-build: ${{env.BUILD_ADMIN}}
@@ -53,7 +53,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: "18"
- name: Install AWS cli
run: |
sudo apt-get update
@@ -79,7 +79,7 @@ jobs:
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-build-space:
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
needs: setup-feature-build
@@ -89,7 +89,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
NEXT_PUBLIC_SPACE_BASE_PATH: "/spaces"
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.space-build }}
@@ -98,7 +98,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: "18"
- name: Install AWS cli
run: |
sudo apt-get update
@@ -134,7 +134,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
NEXT_PUBLIC_ADMIN_BASE_PATH: "/god-mode"
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.admin-build }}
@@ -143,7 +143,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: "18"
- name: Install AWS cli
run: |
sudo apt-get update
@@ -172,7 +172,13 @@ jobs:
feature-deploy:
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true' || needs.setup-feature-build.outputs.admin-build == 'true') }}
needs: [setup-feature-build, feature-build-web, feature-build-space, feature-build-admin]
needs:
[
setup-feature-build,
feature-build-web,
feature-build-space,
feature-build-admin,
]
name: Feature Deploy
runs-on: ubuntu-latest
env:
+1
View File
@@ -81,3 +81,4 @@ tmp/
## packages
dist
.temp/
deploy/selfhost/plane-app/
+7 -7
View File
@@ -7,7 +7,7 @@
</p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source project management that unlocks customer value.</b></p>
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
@@ -40,22 +40,22 @@
</a>
</p>
Meet [Plane](https://dub.sh/plane-website-readme). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘‍♀️
Meet [Plane](https://dub.sh/plane-website-readme), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘‍♀️
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve in our upcoming releases.
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
## ⚡ Installation
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account.
If you want more control over your data, prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/docker-compose).
| Installation Methods | Documentation Link |
| Installation methods | Docs link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
`Instance admins` can configure instance settings with [God-mode](https://docs.plane.so/instance-admin).
## 🚀 Features
+3
View File
@@ -0,0 +1,3 @@
NEXT_PUBLIC_API_BASE_URL=""
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_WEB_BASE_URL=""
+30 -7
View File
@@ -1,3 +1,6 @@
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
@@ -7,6 +10,9 @@ COPY . .
RUN turbo prune --scope=admin --docker
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
# *****************************************************************************
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
@@ -21,13 +27,25 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
RUN yarn turbo run build --filter=admin
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
FROM node:18-alpine AS runner
WORKDIR /app
@@ -38,12 +56,17 @@ COPY --from=installer /app/admin/.next/standalone ./
COPY --from=installer /app/admin/.next/static ./admin/.next/static
COPY --from=installer /app/admin/public ./admin/public
ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
+17
View File
@@ -0,0 +1,17 @@
FROM node:18-alpine
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
COPY . .
RUN yarn global add turbo
RUN yarn install
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
EXPOSE 3000
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
CMD ["yarn", "dev", "--filter=admin"]
+2 -2
View File
@@ -6,7 +6,7 @@ import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@
// components
import { ControllerInput, TControllerInputFormField } from "components/common";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
type IInstanceAIForm = {
config: IFormattedInstanceConfiguration;
@@ -96,7 +96,7 @@ export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
<div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div>
<div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div>
</div>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-16 gap-y-8 lg:grid-cols-3">
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
{aiFormFields.map((field) => (
<ControllerInput
key={field.key}
+1 -1
View File
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
import { PageHeader } from "@/components/core";
import { InstanceAIForm } from "./components";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
const InstanceAIPage = observer(() => {
// store
@@ -3,7 +3,7 @@
import React from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// ui
import { ToggleSwitch } from "@plane/ui";
// types
@@ -3,7 +3,7 @@
import React from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// ui
import { ToggleSwitch } from "@plane/ui";
// types
@@ -2,7 +2,7 @@ import { FC, useState } from "react";
import { useForm } from "react-hook-form";
import Link from "next/link";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components
@@ -4,7 +4,7 @@ import React from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// icons
+1 -1
View File
@@ -11,7 +11,7 @@ import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "../components";
import { InstanceGithubConfigForm } from "./components";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// icons
@@ -2,7 +2,7 @@ import { FC, useState } from "react";
import { useForm } from "react-hook-form";
import Link from "next/link";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components
@@ -4,7 +4,7 @@ import React from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// icons
+1 -1
View File
@@ -10,7 +10,7 @@ import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "../components";
import { InstanceGoogleConfigForm } from "./components";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// icons
import GoogleLogo from "@/public/logos/google-logo.svg";
+1 -1
View File
@@ -14,7 +14,7 @@ import { GoogleConfiguration } from "./google/components";
import { GithubConfiguration } from "./github/components";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// images
+104 -43
View File
@@ -1,9 +1,9 @@
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import React, { FC, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// ui
import { Button, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ControllerInput, TControllerInputFormField } from "components/common";
import { SendTestEmailModal } from "./test-email-modal";
@@ -16,6 +16,14 @@ type IInstanceEmailForm = {
type EmailFormValues = Record<TInstanceEmailConfigurationKeys, string>;
type TEmailSecurityKeys = "EMAIL_USE_TLS" | "EMAIL_USE_SSL" | "NONE";
const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
EMAIL_USE_TLS: "TLS",
EMAIL_USE_SSL: "SSL",
NONE: "No email security",
};
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const { config } = props;
// states
@@ -26,8 +34,9 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const {
handleSubmit,
watch,
setValue,
control,
formState: { errors, isSubmitting },
formState: { errors, isValid, isDirty, isSubmitting },
} = useForm<EmailFormValues>({
defaultValues: {
EMAIL_HOST: config["EMAIL_HOST"],
@@ -35,7 +44,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
EMAIL_HOST_USER: config["EMAIL_HOST_USER"],
EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"],
EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
// EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
EMAIL_FROM: config["EMAIL_FROM"],
},
});
@@ -57,13 +66,26 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
error: Boolean(errors.EMAIL_PORT),
required: true,
},
{
key: "EMAIL_FROM",
type: "text",
label: "Sender email address",
description:
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
placeholder: "no-reply@projectplane.so",
error: Boolean(errors.EMAIL_FROM),
required: true,
},
];
const OptionalEmailFormFields: TControllerInputFormField[] = [
{
key: "EMAIL_HOST_USER",
type: "text",
label: "Username",
placeholder: "getitdone@projectplane.so",
error: Boolean(errors.EMAIL_HOST_USER),
required: true,
required: false,
},
{
key: "EMAIL_HOST_PASSWORD",
@@ -71,17 +93,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
label: "Password",
placeholder: "Password",
error: Boolean(errors.EMAIL_HOST_PASSWORD),
required: true,
},
{
key: "EMAIL_FROM",
type: "text",
label: "From address",
description:
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
placeholder: "no-reply@projectplane.so",
error: Boolean(errors.EMAIL_FROM),
required: true,
required: false,
},
];
@@ -99,11 +111,34 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
.catch((err) => console.error(err));
};
const useTLSValue = watch("EMAIL_USE_TLS");
const useSSLValue = watch("EMAIL_USE_SSL");
const emailSecurityKey: TEmailSecurityKeys = useMemo(() => {
if (useTLSValue === "1") return "EMAIL_USE_TLS";
if (useSSLValue === "1") return "EMAIL_USE_SSL";
return "NONE";
}, [useTLSValue, useSSLValue]);
const handleEmailSecurityChange = (key: TEmailSecurityKeys) => {
if (key === "EMAIL_USE_SSL") {
setValue("EMAIL_USE_TLS", "0");
setValue("EMAIL_USE_SSL", "1");
}
if (key === "EMAIL_USE_TLS") {
setValue("EMAIL_USE_TLS", "1");
setValue("EMAIL_USE_SSL", "0");
}
if (key === "NONE") {
setValue("EMAIL_USE_TLS", "0");
setValue("EMAIL_USE_SSL", "0");
}
};
return (
<div className="space-y-8">
<div>
<SendTestEmailModal isOpen={isSendTestEmailModalOpen} handleClose={() => setIsSendTestEmailModalOpen(false)} />
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-x-20 gap-y-10 lg:grid-cols-2">
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-10 lg:grid-cols-2">
{emailFormFields.map((field) => (
<ControllerInput
key={field.key}
@@ -117,41 +152,67 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
required={field.required}
/>
))}
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">Email security</h4>
<CustomSelect
value={emailSecurityKey}
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
onChange={handleEmailSecurityChange}
buttonClassName="rounded-md border-custom-border-200"
optionsClassName="w-full"
input
>
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
<CustomSelect.Option key={key} value={key} className="w-full">
{value}
</CustomSelect.Option>
))}
</CustomSelect>
</div>
</div>
<div className="flex w-full max-w-md flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
<div className="text-sm font-medium text-custom-text-100">
Turn TLS {Boolean(parseInt(watch("EMAIL_USE_TLS"))) ? "off" : "on"}
</div>
<div className="text-xs font-normal text-custom-text-300">
Use this if your email domain supports TLS.
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
<div className="flex w-full max-w-md flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
<div className="text-sm font-medium text-custom-text-100">Authentication (optional)</div>
<div className="text-xs font-normal text-custom-text-300">
We recommend setting up a username password for your SMTP server
</div>
</div>
</div>
<div className="shrink-0">
<Controller
</div>
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-10 lg:grid-cols-2">
{OptionalEmailFormFields.map((field) => (
<ControllerInput
key={field.key}
control={control}
name="EMAIL_USE_TLS"
render={({ field: { value, onChange } }) => (
<ToggleSwitch
value={Boolean(parseInt(value))}
onChange={() => {
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
}}
size="sm"
/>
)}
type={field.type}
name={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
error={field.error}
required={field.required}
/>
</div>
))}
</div>
</div>
</div>
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
disabled={!isValid || !isDirty}
>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Button variant="outline-primary" onClick={() => setIsSendTestEmailModalOpen(true)} loading={isSubmitting}>
<Button
variant="outline-primary"
onClick={() => setIsSendTestEmailModalOpen(true)}
loading={isSubmitting}
disabled={!isValid}
>
Send test email
</Button>
</div>
+1 -1
View File
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
import { PageHeader } from "@/components/core";
import { InstanceEmailForm } from "./components";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
const InstanceEmailPage = observer(() => {
// store
@@ -6,7 +6,7 @@ import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
// components
import { ControllerInput } from "components/common";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
export interface IGeneralConfigurationForm {
instance: IInstance["instance"];
+1 -1
View File
@@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { PageHeader } from "@/components/core";
import { GeneralConfigurationForm } from "./components";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
const GeneralPage = observer(() => {
const { instance, instanceAdmins } = useInstance();
@@ -5,7 +5,7 @@ import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from
// components
import { ControllerInput } from "components/common";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
type IInstanceImageConfigForm = {
config: IFormattedInstanceConfiguration;
+1 -1
View File
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
import { PageHeader } from "@/components/core";
import { InstanceImageConfigForm } from "./components";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
const InstanceImagePage = observer(() => {
// store
+19
View File
@@ -5,6 +5,10 @@ import { ThemeProvider } from "next-themes";
// lib
import { StoreProvider } from "@/lib/store-context";
import { AppWrapper } from "@/lib/wrappers";
// constants
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
// helpers
import { ASSET_PREFIX } from "@/helpers/common.helper";
// styles
import "./globals.css";
@@ -14,6 +18,21 @@ interface RootLayoutProps {
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => (
<html lang="en">
<head>
<title>{SITE_TITLE}</title>
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:title" content={SITE_TITLE} />
<meta property="og:url" content={SITE_URL} />
<meta name="description" content={SITE_DESCRIPTION} />
<meta property="og:description" content={SITE_DESCRIPTION} />
<meta name="keywords" content={SITE_KEYWORDS} />
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
<link rel="apple-touch-icon" sizes="180x180" href={`${ASSET_PREFIX}/favicon/apple-touch-icon.png`} />
<link rel="icon" type="image/png" sizes="32x32" href={`${ASSET_PREFIX}/favicon/favicon-32x32.png`} />
<link rel="icon" type="image/png" sizes="16x16" href={`${ASSET_PREFIX}/favicon/favicon-16x16.png`} />
<link rel="manifest" href={`${ASSET_PREFIX}/site.webmanifest.json`} />
<link rel="shortcut icon" href={`${ASSET_PREFIX}/favicon/favicon.ico`} />
</head>
<body className={`antialiased`}>
<StoreProvider {...pageProps}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
-19
View File
@@ -1,19 +0,0 @@
"use client";
import { ReactNode } from "react";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
// helpers
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
interface LoginLayoutProps {
children: ReactNode;
}
const LoginLayout = ({ children }: LoginLayoutProps) => (
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>{children}</AuthWrapper>
</InstanceWrapper>
);
export default LoginLayout;
-18
View File
@@ -1,18 +0,0 @@
"use client";
// layouts
import { DefaultLayout } from "@/layouts";
// components
import { PageHeader } from "@/components/core";
import { InstanceSignInForm } from "./components";
const LoginPage = () => (
<>
<PageHeader title="Setup - God Mode" />
<DefaultLayout>
<InstanceSignInForm />
</DefaultLayout>
</>
);
export default LoginPage;
+20 -14
View File
@@ -1,20 +1,26 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
// layouts
import { DefaultLayout } from "@/layouts";
// components
import { PageHeader } from "@/components/core";
import { InstanceSignInForm } from "@/components/login";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
// helpers
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
const RootPage = () => {
const router = useRouter();
const LoginPage = () => (
<>
<PageHeader title="Login - God Mode" />
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>
<DefaultLayout>
<InstanceSignInForm />
</DefaultLayout>
</AuthWrapper>
</InstanceWrapper>
</>
);
useEffect(() => router.push("/login"), [router]);
return (
<>
<PageHeader title="Plane - God Mode" />
</>
);
};
export default RootPage;
export default LoginPage;
+94 -30
View File
@@ -5,13 +5,13 @@ import { useSearchParams } from "next/navigation";
// services
import { AuthService } from "@/services/auth.service";
// ui
import { Button, Checkbox, Input } from "@plane/ui";
import { Button, Checkbox, Input, Spinner } from "@plane/ui";
// components
import { Banner, PasswordStrengthMeter } from "components/common";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
import { API_BASE_URL } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
// service initialization
@@ -39,6 +39,7 @@ type TFormData = {
email: string;
company_name: string;
password: string;
confirm_password?: string;
is_telemetry_enabled: boolean;
};
@@ -66,6 +67,8 @@ export const InstanceSignUpForm: FC = (props) => {
const [showPassword, setShowPassword] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [formData, setFormData] = useState<TFormData>(defaultFromData);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
setFormData((prev) => ({ ...prev, [key]: value }));
@@ -107,18 +110,27 @@ export const InstanceSignUpForm: FC = (props) => {
const isButtonDisabled = useMemo(
() =>
formData.first_name && formData.email && formData.password && getPasswordStrength(formData.password) >= 3
!isSubmitting &&
formData.first_name &&
formData.email &&
formData.password &&
getPasswordStrength(formData.password) >= 3 &&
formData.password === formData.confirm_password
? false
: true,
[formData]
[formData.confirm_password, formData.email, formData.first_name, formData.password, isSubmitting]
);
return (
<div className="relative w-full min-h-full h-auto overflow-hidden container mx-auto px-5 lg:px-0 flex justify-center items-center">
<div className="w-full md:w-4/6 lg:w-3/6 xl:w-2/6 space-y-10">
<div className="relative w-full h-full overflow-hidden container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 flex flex-col justify-center items-center">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1">
<h3 className="text-3xl font-bold">Setup your Plane Instance</h3>
<p className="font-medium text-custom-text-400">Post setup you will be able to manage this Plane instance.</p>
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Setup your Plane Instance
</h3>
<p className="font-medium text-onboarding-text-400">
Post setup you will be able to manage this Plane instance.
</p>
</div>
{errorData.type &&
@@ -127,16 +139,22 @@ export const InstanceSignUpForm: FC = (props) => {
<Banner type="error" message={errorData?.message} />
)}
<form className="space-y-4" method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-up/`}>
<form
className="space-y-4"
method="POST"
action={`${API_BASE_URL}/api/instances/admins/sign-up/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="flex items-center gap-4">
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="first_name">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="first_name">
First name <span className="text-red-500">*</span>
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="first_name"
name="first_name"
type="text"
@@ -144,14 +162,15 @@ export const InstanceSignUpForm: FC = (props) => {
placeholder="Wilber"
value={formData.first_name}
onChange={(e) => handleFormChange("first_name", e.target.value)}
autoFocus
/>
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="last_name">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="last_name">
Last name
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="last_name"
name="last_name"
type="text"
@@ -164,11 +183,11 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email <span className="text-red-500">*</span>
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="email"
name="email"
type="email"
@@ -184,11 +203,11 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="company_name">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="company_name">
Company name
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="company_name"
name="company_name"
type="text"
@@ -200,12 +219,12 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Set a password <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
className={cn("w-full pr-10")}
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="password"
name="password"
type={showPassword ? "text" : "password"}
@@ -214,6 +233,8 @@ export const InstanceSignUpForm: FC = (props) => {
value={formData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
/>
{showPassword ? (
<button
@@ -236,28 +257,71 @@ export const InstanceSignUpForm: FC = (props) => {
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
)}
<PasswordStrengthMeter password={formData.password} />
{isPasswordInputFocused && <PasswordStrengthMeter password={formData.password} />}
</div>
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
</label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
id="confirm_password"
name="confirm_password"
inputSize="md"
value={formData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<button
type="button"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
</button>
) : (
<button
type="button"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
</button>
)}
</div>
{!!formData.confirm_password && formData.password !== formData.confirm_password && (
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
)}
</div>
<div className="relative flex items-center pt-2 gap-2">
<Checkbox
id="is_telemetry_enabled"
name="is_telemetry_enabled"
value={formData.is_telemetry_enabled ? "True" : "False"}
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
checked={formData.is_telemetry_enabled}
/>
<label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
<div>
<Checkbox
id="is_telemetry_enabled"
name="is_telemetry_enabled"
value={formData.is_telemetry_enabled ? "True" : "False"}
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
checked={formData.is_telemetry_enabled}
/>
</div>
<label
className="text-sm text-onboarding-text-300 font-medium cursor-pointer"
htmlFor="is_telemetry_enabled"
>
Allow Plane to anonymously collect usage events.
</label>
<a href="#" className="text-sm font-medium text-blue-500 hover:text-blue-600">
<a href="https://docs.plane.so/telemetry" className="text-sm font-medium text-blue-500 hover:text-blue-600">
See More
</a>
</div>
<div className="py-2">
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
Continue
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</div>
</form>
+43 -35
View File
@@ -1,13 +1,13 @@
"use client";
import { FC, useState, useRef } from "react";
import { Transition } from "@headlessui/react";
import Link from "next/link";
import { FileText, HelpCircle, MoveLeft } from "lucide-react";
import { observer } from "mobx-react-lite";
import { Transition } from "@headlessui/react";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// hooks
import { useTheme } from "@/hooks";
// icons
import { DiscordIcon, GithubIcon } from "@plane/ui";
import { useInstance, useTheme } from "@/hooks/store";
// assets
import packageJson from "package.json";
@@ -29,7 +29,9 @@ const helpOptions = [
},
];
export const HelpSection: FC = () => {
export const HelpSection: FC = observer(() => {
// hooks
const { instance } = useInstance();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store
@@ -37,40 +39,46 @@ export const HelpSection: FC = () => {
// refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
return (
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
isSidebarCollapsed ? "flex-col" : ""
}`}
>
<div
className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full justify-end"}`}
>
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpCircle className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:hidden"
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<MoveLeft className="h-3.5 w-3.5" />
</button>
<button
type="button"
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
</button>
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a
href={redirectionLink}
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 whitespace-nowrap`}
>
<ExternalLink size={14} />
{!isSidebarCollapsed && "Redirect to plane"}
</a>
</Tooltip>
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`ml-auto grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpCircle className="h-3.5 w-3.5" />
</button>
</Tooltip>
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
</button>
</Tooltip>
</div>
<div className="relative">
@@ -123,4 +131,4 @@ export const HelpSection: FC = () => {
</div>
</div>
);
};
});
+1 -1
View File
@@ -3,7 +3,7 @@
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useTheme } from "@/hooks";
import { useTheme } from "@/hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
@@ -1,15 +1,20 @@
"use client";
import { Fragment } from "react";
import { Fragment, useEffect, useState } from "react";
import { useTheme as useNextTheme } from "next-themes";
import { observer } from "mobx-react-lite";
import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
import { Avatar, TOAST_TYPE, setToast } from "@plane/ui";
import { Avatar } from "@plane/ui";
// hooks
import { useTheme, useUser } from "@/hooks";
import { useTheme, useUser } from "@/hooks/store";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// services
import { AuthService } from "@/services";
// service initialization
const authService = new AuthService();
export const SidebarDropdown = observer(() => {
// store hooks
@@ -17,22 +22,60 @@ export const SidebarDropdown = observer(() => {
const { currentUser, signOut } = useUser();
// hooks
const { resolvedTheme, setTheme } = useNextTheme();
const handleSignOut = async () => {
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to sign out. Please try again.",
})
);
};
// state
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const handleThemeSwitch = () => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
setTheme(newTheme);
};
const handleSignOut = () => signOut();
const getSidebarMenuItems = () => (
<Menu.Items
className={cn(
"absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-custom-sidebar-border-100 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none",
{
"left-4": isSidebarCollapsed,
}
)}
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
</div>
<div className="py-2">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleThemeSwitch}
>
<Palette className="h-4 w-4 stroke-[1.5]" />
Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
</Menu.Item>
</div>
<div className="py-2">
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={handleSignOut}>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<Menu.Item
as="button"
type="submit"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
>
<LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out
</Menu.Item>
</form>
</div>
</Menu.Items>
);
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
return (
<div className="flex max-h-[3.75rem] items-center gap-x-5 gap-y-2 border-b border-custom-sidebar-border-200 px-4 py-3.5">
<div className="h-full w-full truncate">
@@ -41,9 +84,30 @@ export const SidebarDropdown = observer(() => {
isSidebarCollapsed ? "justify-center" : ""
}`}
>
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded bg-custom-sidebar-background-80">
<UserCog2 className="h-5 w-5 text-custom-text-200" />
</div>
<Menu as="div" className="flex-shrink-0">
<Menu.Button
className={cn("grid place-items-center outline-none", {
"cursor-default": !isSidebarCollapsed,
})}
>
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded bg-custom-sidebar-background-80">
<UserCog2 className="h-5 w-5 text-custom-text-200" />
</div>
</Menu.Button>
{isSidebarCollapsed && (
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
{getSidebarMenuItems()}
</Transition>
)}
</Menu>
{!isSidebarCollapsed && (
<div className="flex w-full gap-2">
@@ -74,38 +138,7 @@ export const SidebarDropdown = observer(() => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y
divide-custom-sidebar-border-100 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
</div>
<div className="py-2">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleThemeSwitch}
>
<Palette className="h-4 w-4 stroke-[1.5]" />
Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
</Menu.Item>
</div>
<div className="py-2">
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`}>
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out
</Menu.Item>
</form>
</div>
</Menu.Items>
{getSidebarMenuItems()}
</Transition>
</Menu>
)}
@@ -3,7 +3,7 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useTheme } from "@/hooks";
import { useTheme } from "@/hooks/store";
// icons
import { Menu } from "lucide-react";
@@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
import { Tooltip } from "@plane/ui";
// hooks
import { useTheme } from "@/hooks";
import { useTheme } from "@/hooks/store";
// helpers
import { cn } from "@/helpers/common.helper";
+9 -7
View File
@@ -1,5 +1,5 @@
import { FC } from "react";
import { AlertCircle, CheckCircle } from "lucide-react";
import { AlertCircle, CheckCircle2 } from "lucide-react";
type TBanner = {
type: "success" | "error";
@@ -10,19 +10,21 @@ export const Banner: FC<TBanner> = (props) => {
const { type, message } = props;
return (
<div className={`rounded-md p-4 w-full ${type === "error" ? "bg-red-50" : "bg-green-50"}`}>
<div
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-red-500/5 border-red-400" : "bg-green-500/5 border-green-400"}`}
>
<div className="flex items-center justify-center">
<div className="flex-shrink-0">
{type === "error" ? (
<span className="flex items-center justify-center h-6 w-6 bg-red-500 rounded-full">
<AlertCircle className="h-5 w-5 text-white" aria-hidden="true" />
<span className="flex items-center justify-center h-6 w-6 rounded-full">
<AlertCircle className="h-5 w-5 text-red-600" aria-hidden="true" />
</span>
) : (
<CheckCircle className="h-5 w-5 text-green-400" aria-hidden="true" />
<CheckCircle2 className="h-5 w-5 text-green-600" aria-hidden="true" />
)}
</div>
<div className="ml-3">
<p className={`text-sm font-medium ${type === "error" ? "text-red-800" : "text-green-800"} `}>{message}</p>
<div className="ml-1">
<p className={`text-sm font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
</div>
</div>
</div>
+6 -2
View File
@@ -6,6 +6,8 @@ import { Controller, Control } from "react-hook-form";
import { Input } from "@plane/ui";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
type Props = {
control: Control<any>;
@@ -51,7 +53,9 @@ export const ControllerInput: React.FC<Props> = (props) => {
ref={ref}
hasError={error}
placeholder={placeholder}
className="w-full rounded-md font-medium"
className={cn("w-full rounded-md font-medium", {
"pr-10": type === "password",
})}
/>
)}
/>
@@ -72,7 +76,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
</button>
))}
</div>
{description && <p className="text-xs text-custom-text-400">{description}</p>}
{description && <p className="text-xs text-custom-text-300">{description}</p>}
</div>
);
};
+46
View File
@@ -0,0 +1,46 @@
import React from "react";
import Image from "next/image";
import { Button } from "@plane/ui";
type Props = {
title: string;
description?: React.ReactNode;
image?: any;
primaryButton?: {
icon?: any;
text: string;
onClick: () => void;
};
secondaryButton?: React.ReactNode;
disabled?: boolean;
};
export const EmptyState: React.FC<Props> = ({
title,
description,
image,
primaryButton,
secondaryButton,
disabled = false,
}) => (
<div className={`flex h-full w-full items-center justify-center`}>
<div className="flex w-full flex-col items-center text-center">
{image && <Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
<div className="flex items-center gap-4">
{primaryButton && (
<Button
variant="primary"
prependIcon={primaryButton.icon}
onClick={primaryButton.onClick}
disabled={disabled}
>
{primaryButton.text}
</Button>
)}
{secondaryButton}
</div>
</div>
</div>
);
+1
View File
@@ -4,3 +4,4 @@ export * from "./controller-input";
export * from "./copy-field";
export * from "./password-strength-meter";
export * from "./banner";
export * from "./empty-state";
+3 -3
View File
@@ -1,4 +1,4 @@
import Head from "next/head";
"use client";
type TPageHeader = {
title?: string;
@@ -9,9 +9,9 @@ export const PageHeader: React.FC<TPageHeader> = (props) => {
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
return (
<Head>
<>
<title>{title}</title>
<meta name="description" content={description} />
</Head>
</>
);
};
@@ -11,9 +11,9 @@ export const InstanceNotReady: FC = () => (
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-3xl font-bold">Welcome aboard Plane!</h1>
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-custom-text-400">
<p className="font-medium text-base text-onboarding-text-400">
Get started by setting up your instance and workspace
</p>
</div>
@@ -5,13 +5,13 @@ import { useSearchParams } from "next/navigation";
// services
import { AuthService } from "@/services/auth.service";
// ui
import { Button, Input } from "@plane/ui";
import { Button, Input, Spinner } from "@plane/ui";
// components
import { Banner } from "components/common";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
import { API_BASE_URL } from "@/helpers/common.helper";
// service initialization
const authService = new AuthService();
@@ -52,6 +52,7 @@ export const InstanceSignInForm: FC = (props) => {
const [showPassword, setShowPassword] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [formData, setFormData] = useState<TFormData>(defaultFromData);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
setFormData((prev) => ({ ...prev, [key]: value }));
@@ -85,27 +86,40 @@ export const InstanceSignInForm: FC = (props) => {
} else return { type: undefined, message: undefined };
}, [errorCode, errorMessage]);
const isButtonDisabled = useMemo(() => (formData.email && formData.password ? false : true), [formData]);
const isButtonDisabled = useMemo(
() => (!isSubmitting && formData.email && formData.password ? false : true),
[formData.email, formData.password, isSubmitting]
);
return (
<div className="relative w-full h-full overflow-hidden container mx-auto px-5 md:px-0 flex justify-center items-center">
<div className="w-full md:w-4/6 lg:w-3/6 xl:w-2/6 space-y-10">
<div className="relative w-full h-full overflow-hidden container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 flex flex-col justify-center items-center">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1">
<h3 className="text-3xl font-bold">Manage your Plane instance</h3>
<p className="font-medium text-custom-text-400">Configure instance-wide settings to secure your instance</p>
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Manage your Plane instance
</h3>
<p className="font-medium text-onboarding-text-400">
Configure instance-wide settings to secure your instance
</p>
</div>
{errorData.type && errorData?.message && <Banner type="error" message={errorData?.message} />}
<form className="space-y-4" method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-in/`}>
<form
className="space-y-4"
method="POST"
action={`${API_BASE_URL}/api/instances/admins/sign-in/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
Email <span className="text-red-500">*</span>
</label>
<Input
className="w-full"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="email"
name="email"
type="email"
@@ -113,16 +127,17 @@ export const InstanceSignInForm: FC = (props) => {
placeholder="name@company.com"
value={formData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
autoFocus
/>
</div>
<div className="w-full space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
className={cn("w-full pr-10")}
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
id="password"
name="password"
type={showPassword ? "text" : "password"}
@@ -152,7 +167,7 @@ export const InstanceSignInForm: FC = (props) => {
</div>
<div className="py-2">
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
Sign in
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
</Button>
</div>
</form>
@@ -2,35 +2,30 @@
import React from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import Image from "next/image";
import { useTheme } from "next-themes";
import { useTheme as nextUseTheme } from "next-themes";
// ui
import { Button, getButtonStyling } from "@plane/ui";
// helpers
import { resolveGeneralTheme } from "helpers/common.helper";
// hooks
import { useInstance, useTheme } from "@/hooks/store";
// icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
type Props = {
isOpen: boolean;
onClose?: () => void;
};
export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
const { isOpen, onClose } = props;
export const NewUserPopup: React.FC = observer(() => {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
const { instance } = useInstance();
// theme
const { resolvedTheme } = useTheme();
const { resolvedTheme } = nextUseTheme();
const handleClose = () => {
onClose && onClose();
};
if (!isOpen) return null;
const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
if (!isNewUserPopup) return <></>;
return (
<div className="absolute bottom-8 right-6 p-6 w-96 border border-custom-border-100 shadow-md rounded-xl bg-custom-background-100 z-20">
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
<div className="flex gap-4">
<div className="grow">
<div className="text-base font-semibold">Create workspace</div>
@@ -39,10 +34,10 @@ export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
workspace, you will need to login again.
</div>
<div className="flex items-center gap-4 pt-2">
<Link href="/create-workspace" className={getButtonStyling("primary", "sm")}>
<a href={redirectionLink} className={getButtonStyling("primary", "sm")}>
Create workspace
</Link>
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
</a>
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
Close
</Button>
</div>
+8
View File
@@ -0,0 +1,8 @@
export const SITE_NAME = "Plane | Simple, extensible, open-source project management tool.";
export const SITE_TITLE = "Plane | Simple, extensible, open-source project management tool.";
export const SITE_DESCRIPTION =
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.";
export const SITE_KEYWORDS =
"software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
export const SITE_URL = "https://app.plane.so/";
export const TWITTER_USER_NAME = "Plane | Simple, extensible, open-source project management tool.";
+126
View File
@@ -0,0 +1,126 @@
import { ReactNode } from "react";
export enum EPageTypes {
"PUBLIC" = "PUBLIC",
"NON_AUTHENTICATED" = "NON_AUTHENTICATED",
"ONBOARDING" = "ONBOARDING",
"AUTHENTICATED" = "AUTHENTICATED",
}
export enum EAuthModes {
SIGN_IN = "SIGN_IN",
SIGN_UP = "SIGN_UP",
}
export enum EAuthSteps {
EMAIL = "EMAIL",
PASSWORD = "PASSWORD",
UNIQUE_CODE = "UNIQUE_CODE",
}
export enum EAuthenticationErrorCodes {
INSTANCE_NOT_CONFIGURED = "5000",
// Admin
ADMIN_ALREADY_EXIST = "5029",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5030",
INVALID_ADMIN_EMAIL = "5031",
INVALID_ADMIN_PASSWORD = "5032",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5033",
ADMIN_AUTHENTICATION_FAILED = "5034",
ADMIN_USER_ALREADY_EXIST = "5035",
ADMIN_USER_DOES_NOT_EXIST = "5036",
}
export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT",
TOAST_ALERT = "TOAST_ALERT",
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
INLINE_EMAIL = "INLINE_EMAIL",
INLINE_PASSWORD = "INLINE_PASSWORD",
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
}
export type TAuthErrorInfo = {
type: EErrorAlertType;
code: EAuthenticationErrorCodes;
title: string;
message: ReactNode;
};
const errorCodeMessages: {
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = {
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: {
title: "Instance not configured",
message: () => "Please contact your administrator to configure the instance.",
},
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
title: "Admin already exists",
message: () => "Admin already exists. Please sign in.",
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: "Required",
message: () => "Please enter email, password and first name.",
},
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
title: "Invalid email",
message: () => "Please enter a valid email.",
},
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: "Invalid password",
message: () => "Password must be at least 8 characters long.",
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: "Required",
message: () => "Please enter email and password.",
},
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: "Authentication failed",
message: () => "Please check your email and password and try again.",
},
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: "User already exists",
message: () => "User already exists. Please sign in.",
},
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: "User does not exist",
message: () => "User does not exist. Please sign up.",
},
};
export const authErrorHandler = (
errorCode: EAuthenticationErrorCodes,
email?: string | undefined
): TAuthErrorInfo | undefined => {
const toastAlertErrorCodes = [
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
];
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
];
if (toastAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.TOAST_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
if (bannerAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.BANNER_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
return undefined;
};
+10 -1
View File
@@ -1,7 +1,16 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
export const ASSET_PREFIX = ADMIN_BASE_PATH;
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
-6
View File
@@ -1,6 +0,0 @@
export * from "./use-outside-click-detector";
// store-hooks
export * from "./store/use-theme";
export * from "./store/use-instance";
export * from "./store/use-user";
+3
View File
@@ -0,0 +1,3 @@
export * from "./use-theme";
export * from "./use-instance";
export * from "./use-user";
+3
View File
@@ -1,6 +1,8 @@
import { FC, ReactNode } from "react";
// components
import { InstanceSidebar } from "@/components/admin-sidebar";
import { InstanceHeader } from "@/components/auth-header";
import { NewUserPopup } from "@/components/new-user-popup";
type TAdminLayout = {
children: ReactNode;
@@ -16,6 +18,7 @@ export const AdminLayout: FC<TAdminLayout> = (props) => {
<InstanceHeader />
<div className="h-full w-full overflow-hidden">{children}</div>
</main>
<NewUserPopup />
</div>
);
};
+23 -12
View File
@@ -2,31 +2,42 @@
import { FC, ReactNode } from "react";
import Image from "next/image";
import { usePathname } from "next/navigation";
// logo
import { useTheme } from "next-themes";
// logo/ images
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
type TDefaultLayout = {
children: ReactNode;
withoutBackground?: boolean;
};
export const DefaultLayout: FC<TDefaultLayout> = (props) => {
const { children } = props;
const pathname = usePathname();
console.log("pathname", pathname);
const { children, withoutBackground = false } = props;
// hooks
const { resolvedTheme } = useTheme();
return (
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="flex-shrink-0 h-[120px]">
<div className="relative h-full container mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="flex items-center gap-x-2">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<div className="relative">
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
{!withoutBackground && (
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen h-full object-cover"
alt="Plane background pattern"
/>
</div>
)}
<div className="relative z-10 mb-[110px] flex-grow">{children}</div>
</div>
<div className="w-full flex-grow">{children}</div>
</div>
);
};
+1 -1
View File
@@ -4,7 +4,7 @@ import { ReactElement, createContext } from "react";
// mobx store
import { RootStore } from "@/store/root-store";
let rootStore = new RootStore();
export let rootStore = new RootStore();
export const StoreContext = createContext<RootStore>(rootStore);
+2 -2
View File
@@ -4,11 +4,11 @@ import { FC, ReactNode, useEffect, Suspense } from "react";
import { observer } from "mobx-react-lite";
import { SWRConfig } from "swr";
// hooks
import { useTheme, useUser } from "@/hooks";
import { useTheme, useUser } from "@/hooks/store";
// ui
import { Toast } from "@plane/ui";
// constants
import { SWR_CONFIG } from "constants/swr-config";
import { SWR_CONFIG } from "@/constants/swr-config";
// helpers
import { resolveGeneralTheme } from "helpers/common.helper";
+25 -25
View File
@@ -1,14 +1,14 @@
"use client";
import { FC, ReactNode } from "react";
import { useRouter } from "next/navigation";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Spinner } from "@plane/ui";
// hooks
import { useInstance, useUser } from "@/hooks";
import { useInstance, useUser } from "@/hooks/store";
// helpers
import { EAuthenticationPageType, EUserStatus } from "@/helpers";
import { redirect } from "next/navigation";
import { EAuthenticationPageType } from "@/helpers";
export interface IAuthWrapper {
children: ReactNode;
@@ -16,41 +16,41 @@ export interface IAuthWrapper {
}
export const AuthWrapper: FC<IAuthWrapper> = observer((props) => {
const router = useRouter();
// props
const { children, authType = EAuthenticationPageType.AUTHENTICATED } = props;
// hooks
const { instance, fetchInstanceAdmins } = useInstance();
const { isLoading, userStatus, currentUser, fetchCurrentUser } = useUser();
const { instance } = useInstance();
const { isLoading, currentUser, fetchCurrentUser } = useUser();
useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
shouldRetryOnError: false,
});
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins(), {
const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
shouldRetryOnError: false,
});
if (isLoading)
if (isSWRLoading || isLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
</div>
);
if (userStatus && userStatus?.status === EUserStatus.ERROR)
return (
<div className="relative flex h-screen w-screen items-center justify-center">
Something went wrong. please try again later
</div>
);
if (authType === EAuthenticationPageType.NOT_AUTHENTICATED) {
if (currentUser === undefined) return <>{children}</>;
else {
router.push("/general/");
return <></>;
}
}
if ([EAuthenticationPageType.AUTHENTICATED, EAuthenticationPageType.NOT_AUTHENTICATED].includes(authType)) {
if (authType === EAuthenticationPageType.NOT_AUTHENTICATED) {
if (currentUser === undefined) return <>{children}</>;
else redirect("/general/");
} else {
if (currentUser) return <>{children}</>;
else {
if (instance?.instance?.is_setup_done) redirect("/login/");
else redirect("/setup/");
if (authType === EAuthenticationPageType.AUTHENTICATED) {
if (currentUser) return <>{children}</>;
else {
if (instance && instance?.instance?.is_setup_done) {
router.push("/");
return <></>;
} else {
router.push("/setup/");
return <></>;
}
}
}
+17 -10
View File
@@ -10,9 +10,10 @@ import { DefaultLayout } from "@/layouts";
// components
import { InstanceNotReady } from "@/components/instance";
// hooks
import { useInstance } from "@/hooks";
import { useInstance } from "@/hooks/store";
// helpers
import { EInstancePageType, EInstanceStatus } from "@/helpers";
import { EInstancePageType } from "@/helpers";
import { EmptyState } from "@/components/common";
type TInstanceWrapper = {
children: ReactNode;
@@ -24,34 +25,40 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
const searchparams = useSearchParams();
const authEnabled = searchparams.get("auth_enabled") || "1";
// hooks
const { isLoading, instanceStatus, instance, fetchInstanceInfo } = useInstance();
const { isLoading, instance, fetchInstanceInfo } = useInstance();
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
errorRetryCount: 0,
});
if (isLoading)
if (isSWRLoading || isLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
</div>
);
if (instanceStatus && instanceStatus?.status === EInstanceStatus.ERROR)
if (!instance) {
return (
<div className="relative flex h-screen w-screen items-center justify-center">
Something went wrong. please try again later
</div>
<EmptyState
title="Your instance wasn't configured successfully."
description="Please try re-installing Plane to fix the problem. If the issue still persists please reach out to support@plane.so."
/>
);
}
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
return (
<DefaultLayout>
<DefaultLayout withoutBackground>
<InstanceNotReady />
</DefaultLayout>
);
if (instance?.instance?.is_setup_done && pageType === EInstancePageType.PRE_SETUP) redirect("/");
if (!instance?.instance?.is_setup_done && pageType === EInstancePageType.POST_SETUP) redirect("/setup");
return <>{children}</>;
+1 -1
View File
@@ -7,7 +7,7 @@ const nextConfig = {
images: {
unoptimized: true,
},
basePath: process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? "/god-mode" : "",
basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
};
module.exports = nextConfig;
+5 -4
View File
@@ -4,13 +4,14 @@
"private": true,
"scripts": {
"dev": "turbo run develop",
"develop": "next dev --port 3333",
"develop": "next dev --port 3001",
"build": "next build",
"preview": "next build && next start",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@headlessui/react": "^1.7.19",
"@plane/types": "*",
"@plane/ui": "*",
"@tailwindcss/typography": "^0.5.9",
@@ -22,11 +23,11 @@
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
"mobx-react-lite": "^4.0.5",
"next": "^14.1.0",
"next": "^14.2.3",
"next-themes": "^0.2.1",
"postcss": "8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.0",
"swr": "^2.2.4",
"tailwindcss": "3.3.2",

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

+11
View File
@@ -0,0 +1,11 @@
{
"name": "",
"short_name": "",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Plane God Mode",
"short_name": "Plane God Mode",
"description": "Plane helps you plan your issues, cycles, and product modules.",
"start_url": ".",
"display": "standalone",
"background_color": "#f9fafb",
"theme_color": "#3f76ff",
"icons": [
{ "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}
+5 -2
View File
@@ -1,4 +1,6 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
// store
import { rootStore } from "@/lib/store-context";
export abstract class APIService {
protected baseURL: string;
@@ -18,8 +20,9 @@ export abstract class APIService {
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) window.location.href = "/login";
return Promise.reject(error.response?.data ?? error);
const store = rootStore;
if (error.response && error.response.status === 401 && store.user.currentUser) store.user.reset();
return Promise.reject(error);
}
);
}
-23
View File
@@ -19,27 +19,4 @@ export class AuthService extends APIService {
throw error;
});
}
async signOut(baseUrl: string): Promise<any> {
await this.requestCSRFToken().then((data) => {
const csrfToken = data?.csrf_token;
if (!csrfToken) throw Error("CSRF token not found");
var form = document.createElement("form");
var element1 = document.createElement("input");
form.method = "POST";
form.action = `${baseUrl}/api/instances/admins/sign-out/`;
element1.value = csrfToken;
element1.name = "csrfmiddlewaretoken";
element1.type = "hidden";
form.appendChild(element1);
document.body.appendChild(form);
form.submit();
});
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { APIService } from "services/api.service";
// types
import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types";
// helpers
import { API_BASE_URL } from "helpers/common.helper";
import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
export class InstanceService extends APIService {
constructor() {
+2
View File
@@ -75,6 +75,8 @@ export class InstanceStore implements IInstanceStore {
try {
if (this.instance === undefined) this.isLoading = true;
const instance = await this.instanceService.getInstanceInfo();
// handling the new user popup toggle
if (this.instance === undefined && !instance?.instance?.workspaces_exist) this.store.theme.toggleNewUserPopup();
runInAction(() => {
this.isLoading = false;
this.instance = instance;
+3 -1
View File
@@ -18,8 +18,10 @@ export class RootStore {
}
resetOnSignOut() {
this.theme = new ThemeStore(this);
localStorage.setItem("theme", "system");
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.theme = new ThemeStore(this);
}
}
+12 -2
View File
@@ -5,31 +5,41 @@ import { RootStore } from "@/store/root-store";
type TTheme = "dark" | "light";
export interface IThemeStore {
// observables
isNewUserPopup: boolean;
theme: string | undefined;
isSidebarCollapsed: boolean | undefined;
// actions
toggleNewUserPopup: () => void;
toggleSidebar: (collapsed: boolean) => void;
setTheme: (currentTheme: TTheme) => void;
}
export class ThemeStore implements IThemeStore {
// observables
isNewUserPopup: boolean = false;
isSidebarCollapsed: boolean | undefined = undefined;
theme: string | undefined = undefined;
constructor(private store: RootStore) {
makeObservable(this, {
// observables
isNewUserPopup: observable.ref,
isSidebarCollapsed: observable.ref,
theme: observable.ref,
// action
toggleNewUserPopup: action,
toggleSidebar: action,
setTheme: action,
});
}
/**
* Toggle the sidebar collapsed state
* @description Toggle the new user popup modal
*/
toggleNewUserPopup = () => (this.isNewUserPopup = !this.isNewUserPopup);
/**
* @description Toggle the sidebar collapsed state
* @param isCollapsed
*/
toggleSidebar = (isCollapsed: boolean) => {
@@ -39,7 +49,7 @@ export class ThemeStore implements IThemeStore {
};
/**
* Sets the user theme and applies it to the platform
* @description Sets the user theme and applies it to the platform
* @param currentTheme
*/
setTheme = async (currentTheme: TTheme) => {
+27 -13
View File
@@ -3,11 +3,10 @@ import { IUser } from "@plane/types";
// helpers
import { EUserStatus, TUserStatus } from "@/helpers";
// services
import { UserService } from "services/user.service";
import { UserService } from "@/services/user.service";
// root store
import { RootStore } from "@/store/root-store";
import { AuthService } from "@/services";
import { API_BASE_URL } from "@/helpers/common.helper";
export interface IUserStore {
// observables
@@ -17,7 +16,8 @@ export interface IUserStore {
currentUser: IUser | undefined;
// fetch actions
fetchCurrentUser: () => Promise<IUser>;
signOut: () => Promise<void>;
reset: () => void;
signOut: () => void;
}
export class UserStore implements IUserStore {
@@ -29,8 +29,6 @@ export class UserStore implements IUserStore {
// services
userService;
authService;
// rootStore
rootStore;
constructor(private store: RootStore) {
makeObservable(this, {
@@ -41,10 +39,11 @@ export class UserStore implements IUserStore {
currentUser: observable,
// action
fetchCurrentUser: action,
reset: action,
signOut: action,
});
this.userService = new UserService();
this.authService = new AuthService();
this.rootStore = store;
}
/**
@@ -55,11 +54,20 @@ export class UserStore implements IUserStore {
try {
if (this.currentUser === undefined) this.isLoading = true;
const currentUser = await this.userService.currentUser();
runInAction(() => {
this.isUserLoggedIn = true;
this.currentUser = currentUser;
this.isLoading = false;
});
if (currentUser) {
await this.store.instance.fetchInstanceAdmins();
runInAction(() => {
this.isUserLoggedIn = true;
this.currentUser = currentUser;
this.isLoading = false;
});
} else {
runInAction(() => {
this.isUserLoggedIn = false;
this.currentUser = undefined;
this.isLoading = false;
});
}
return currentUser;
} catch (error: any) {
this.isLoading = false;
@@ -78,8 +86,14 @@ export class UserStore implements IUserStore {
}
};
reset = async () => {
this.isUserLoggedIn = false;
this.currentUser = undefined;
this.isLoading = false;
this.userStatus = undefined;
};
signOut = async () => {
await this.authService.signOut(API_BASE_URL);
this.rootStore.resetOnSignOut();
this.store.resetOnSignOut();
};
}
+6 -1
View File
@@ -1,7 +1,7 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
CORS_ALLOWED_ORIGINS=""
CORS_ALLOWED_ORIGINS="http://localhost"
# Error logs
SENTRY_DSN=""
@@ -44,3 +44,8 @@ WEB_URL="http://localhost"
# Gunicorn Workers
GUNICORN_WORKERS=2
# Base URLs
ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.18.0"
"version": "0.19.0"
}
+4
View File
@@ -1,9 +1,13 @@
# Module improts
from .base import BaseSerializer
from .issue import IssueExpandSerializer
from plane.db.models import InboxIssue
class InboxIssueSerializer(BaseSerializer):
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
class Meta:
model = InboxIssue
fields = "__all__"
-37
View File
@@ -1,6 +1,4 @@
# Python imports
from urllib.parse import urlparse
import zoneinfo
# Django imports
@@ -19,7 +17,6 @@ from rest_framework.views import APIView
# Module imports
from plane.api.middleware.api_authentication import APIKeyAuthentication
from plane.api.rate_limit import ApiKeyRateThrottle
from plane.bgtasks.webhook_task import send_webhook
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
@@ -38,40 +35,6 @@ class TimezoneMixin:
timezone.deactivate()
class WebhookMixin:
webhook_event = None
bulk = False
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(
request, response, *args, **kwargs
)
# Check for the case should webhook be sent
if (
self.webhook_event
and self.request.method in ["POST", "PATCH", "DELETE"]
and response.status_code in [200, 201, 204]
):
url = request.build_absolute_uri()
parsed_url = urlparse(url)
# Extract the scheme and netloc
scheme = parsed_url.scheme
netloc = parsed_url.netloc
# Push the object to delay
send_webhook.delay(
event=self.webhook_event,
payload=response.data,
kw=self.kwargs,
action=self.request.method,
slug=self.workspace_slug,
bulk=self.bulk,
current_site=f"{scheme}://{netloc}",
)
return response
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
authentication_classes = [
APIKeyAuthentication,
+31 -3
View File
@@ -5,6 +5,7 @@ import json
from django.core import serializers
from django.db.models import Count, F, Func, OuterRef, Q, Sum
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
@@ -26,10 +27,11 @@ from plane.db.models import (
)
from plane.utils.analytics_plot import burndown_plot
from .base import BaseAPIView, WebhookMixin
from .base import BaseAPIView
from plane.bgtasks.webhook_task import model_activity
class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
class CycleAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to cycle.
@@ -277,6 +279,16 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
project_id=project_id,
owned_by=request.user,
)
# Send the model activity
model_activity.delay(
model_name="cycle",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
@@ -295,6 +307,11 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
)
if cycle.archived_at:
return Response(
{"error": "Archived cycle cannot be edited"},
@@ -344,6 +361,17 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
status=status.HTTP_409_CONFLICT,
)
serializer.save()
# Send the model activity
model_activity.delay(
model_name="cycle",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -515,7 +543,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
class CycleIssueAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`,
and `destroy` actions related to cycle issues.
+10 -8
View File
@@ -154,6 +154,13 @@ class InboxIssueAPIEndpoint(BaseAPIView):
state=state,
)
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
)
# Create an Issue Activity
issue_activity.delay(
type="issue.activity.created",
@@ -163,14 +170,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
inbox=str(inbox_issue.id),
)
serializer = InboxIssueSerializer(inbox_issue)
@@ -260,6 +260,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
inbox=(inbox_issue.id),
)
issue_serializer.save()
else:
@@ -327,6 +328,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
)
return Response(serializer.data, status=status.HTTP_200_OK)
+14 -12
View File
@@ -32,7 +32,6 @@ from plane.api.serializers import (
LabelSerializer,
)
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
ProjectLitePermission,
ProjectMemberPermission,
@@ -49,11 +48,10 @@ from plane.db.models import (
ProjectMember,
)
from .base import BaseAPIView, WebhookMixin
from .base import BaseAPIView
class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
class WorkspaceIssueAPIEndpoint(BaseAPIView):
"""
This viewset provides `retrieveByIssueId` on workspace level
@@ -61,12 +59,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
model = Issue
webhook_event = "issue"
permission_classes = [
ProjectEntityPermission
]
permission_classes = [ProjectEntityPermission]
serializer_class = IssueSerializer
@property
def project__identifier(self):
return self.kwargs.get("project__identifier", None)
@@ -92,7 +87,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()
def get(self, request, slug, project__identifier=None, issue__identifier=None):
def get(
self, request, slug, project__identifier=None, issue__identifier=None
):
if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
@@ -101,7 +98,11 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(workspace__slug=slug, project__identifier=project__identifier, sequence_id=issue__identifier)
).get(
workspace__slug=slug,
project__identifier=project__identifier,
sequence_id=issue__identifier,
)
return Response(
IssueSerializer(
issue,
@@ -111,7 +112,8 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
status=status.HTTP_200_OK,
)
class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
class IssueAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to issue.
@@ -653,7 +655,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
class IssueCommentAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to comments of the particular issue.
+32 -3
View File
@@ -5,6 +5,7 @@ import json
from django.core import serializers
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
@@ -28,10 +29,11 @@ from plane.db.models import (
Project,
)
from .base import BaseAPIView, WebhookMixin
from .base import BaseAPIView
from plane.bgtasks.webhook_task import model_activity
class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
class ModuleAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to module.
@@ -163,6 +165,16 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
status=status.HTTP_409_CONFLICT,
)
serializer.save()
# Send the model activity
model_activity.delay(
model_name="module",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
module = Module.objects.get(pk=serializer.data["id"])
serializer = ModuleSerializer(module)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -172,6 +184,11 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
current_instance = json.dumps(
ModuleSerializer(module).data, cls=DjangoJSONEncoder
)
if module.archived_at:
return Response(
{"error": "Archived module cannot be edited"},
@@ -204,6 +221,18 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
status=status.HTTP_409_CONFLICT,
)
serializer.save()
# Send the model activity
model_activity.delay(
model_name="module",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -260,7 +289,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
class ModuleIssueAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to module issues.
+32 -4
View File
@@ -1,7 +1,11 @@
# Python imports
import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
@@ -23,11 +27,11 @@ from plane.db.models import (
State,
Workspace,
)
from .base import BaseAPIView, WebhookMixin
from plane.bgtasks.webhook_task import model_activity
from .base import BaseAPIView
class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
class ProjectAPIEndpoint(BaseAPIView):
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
serializer_class = ProjectSerializer
@@ -236,6 +240,17 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
.filter(pk=serializer.data["id"])
.first()
)
# Model activity
model_activity.delay(
model_name="project",
model_id=str(project.id),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
serializer = ProjectSerializer(project)
return Response(
serializer.data, status=status.HTTP_201_CREATED
@@ -265,7 +280,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
try:
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=pk)
current_instance = json.dumps(
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
if project.archived_at:
return Response(
{"error": "Archived project cannot be updated"},
@@ -303,6 +320,17 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
.filter(pk=serializer.data["id"])
.first()
)
model_activity.delay(
model_name="project",
model_id=str(project.id),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
+2 -1
View File
@@ -442,7 +442,7 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError("Invalid URL format.")
# Check URL scheme
if not value.startswith(('http://', 'https://')):
if not value.startswith(("http://", "https://")):
raise serializers.ValidationError("Invalid URL scheme.")
return value
@@ -636,6 +636,7 @@ class IssueInboxSerializer(DynamicBaseSerializer):
"project_id",
"created_at",
"label_ids",
"created_by",
]
read_only_fields = fields
+1 -1
View File
@@ -29,7 +29,7 @@ from .user.base import (
)
from .base import BaseAPIView, BaseViewSet, WebhookMixin
from .base import BaseAPIView, BaseViewSet
from .workspace.base import (
WorkSpaceViewSet,
-30
View File
@@ -20,7 +20,6 @@ from rest_framework.viewsets import ModelViewSet
# Module imports
from plane.authentication.session import BaseSessionAuthentication
from plane.bgtasks.webhook_task import send_webhook
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
@@ -39,35 +38,6 @@ class TimezoneMixin:
timezone.deactivate()
class WebhookMixin:
webhook_event = None
bulk = False
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(
request, response, *args, **kwargs
)
# Check for the case should webhook be sent
if (
self.webhook_event
and self.request.method in ["POST", "PATCH", "DELETE"]
and response.status_code in [200, 201, 204]
):
# Push the object to delay
send_webhook.delay(
event=self.webhook_event,
payload=response.data,
kw=self.kwargs,
action=self.request.method,
slug=self.workspace_slug,
bulk=self.bulk,
current_site=request.META.get("HTTP_ORIGIN"),
)
return response
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
model = None
+32 -2
View File
@@ -20,6 +20,7 @@ from django.db.models import (
)
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
@@ -47,10 +48,11 @@ from plane.db.models import (
from plane.utils.analytics_plot import burndown_plot
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
from .. import BaseAPIView, BaseViewSet
from plane.bgtasks.webhook_task import model_activity
class CycleViewSet(WebhookMixin, BaseViewSet):
class CycleViewSet(BaseViewSet):
serializer_class = CycleSerializer
model = Cycle
webhook_event = "cycle"
@@ -412,6 +414,17 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
)
.first()
)
# Send the model activity
model_activity.delay(
model_name="cycle",
model_id=str(cycle["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(cycle, status=status.HTTP_201_CREATED)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
@@ -434,6 +447,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
{"error": "Archived cycle cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
)
request_data = request.data
if (
@@ -487,6 +505,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"assignee_ids",
"status",
).first()
# Send the model activity
model_activity.delay(
model_name="cycle",
model_id=str(cycle["id"]),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(cycle, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+10 -4
View File
@@ -23,7 +23,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet, WebhookMixin
from .. import BaseViewSet
from plane.app.serializers import (
IssueSerializer,
CycleIssueSerializer,
@@ -38,9 +38,9 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
class CycleIssueViewSet(BaseViewSet):
serializer_class = CycleIssueSerializer
model = CycleIssue
@@ -191,6 +191,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, cycle_id):
@@ -249,6 +254,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
update_cycle_issue_activity = []
# Iterate over each cycle_issue in cycle_issues
for cycle_issue in cycle_issues:
old_cycle_id = cycle_issue.cycle_id
# Update the cycle_issue's cycle_id
cycle_issue.cycle_id = cycle_id
# Add the modified cycle_issue to the records_to_update list
@@ -256,7 +262,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
# Record the update activity
update_cycle_issue_activity.append(
{
"old_cycle_id": str(cycle_issue.cycle_id),
"old_cycle_id": str(old_cycle_id),
"new_cycle_id": str(cycle_id),
"issue_id": str(cycle_issue.issue_id),
}
+33 -12
View File
@@ -251,6 +251,16 @@ class InboxIssueViewSet(BaseViewSet):
)
if serializer.is_valid():
serializer.save()
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "in-app"),
)
# Create an Issue Activity
issue_activity.delay(
type="issue.activity.created",
@@ -262,16 +272,7 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "in-app"),
inbox=str(inbox_issue.id),
)
inbox_issue = (
InboxIssue.objects.select_related("issue")
@@ -339,7 +340,24 @@ class InboxIssueViewSet(BaseViewSet):
# Get issue data
issue_data = request.data.pop("issue", False)
if bool(issue_data):
issue = Issue.objects.get(
issue = Issue.objects.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
).get(
pk=inbox_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
@@ -379,6 +397,7 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
)
issue_serializer.save()
else:
@@ -444,6 +463,7 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
inbox=(inbox_issue.id),
)
inbox_issue = (
@@ -480,7 +500,8 @@ class InboxIssueViewSet(BaseViewSet):
output_field=ArrayField(UUIDField()),
),
),
).first()
)
.first()
)
serializer = InboxIssueDetailSerializer(inbox_issue).data
return Response(serializer, status=status.HTTP_200_OK)
+6 -1
View File
@@ -47,7 +47,7 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
class IssueArchiveViewSet(BaseViewSet):
permission_classes = [
@@ -239,6 +239,11 @@ class IssueArchiveViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None):
+15 -2
View File
@@ -50,9 +50,10 @@ from plane.db.models import (
Project,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
from .. import BaseAPIView, BaseViewSet
class IssueListEndpoint(BaseAPIView):
@@ -241,10 +242,14 @@ class IssueListEndpoint(BaseAPIView):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
class IssueViewSet(WebhookMixin, BaseViewSet):
class IssueViewSet(BaseViewSet):
def get_serializer_class(self):
return (
IssueCreateSerializer
@@ -440,6 +445,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
@@ -503,6 +512,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
)
.first()
)
datetime_fields = ["created_at", "updated_at"]
issue = user_timezone_converter(
issue, datetime_fields, request.user.user_timezone
)
return Response(issue, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+2 -2
View File
@@ -11,7 +11,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet, WebhookMixin
from .. import BaseViewSet
from plane.app.serializers import (
IssueCommentSerializer,
CommentReactionSerializer,
@@ -25,7 +25,7 @@ from plane.db.models import (
from plane.bgtasks.issue_activites_task import issue_activity
class IssueCommentViewSet(WebhookMixin, BaseViewSet):
class IssueCommentViewSet(BaseViewSet):
serializer_class = IssueCommentSerializer
model = IssueComment
webhook_event = "issue_comment"
+5
View File
@@ -45,6 +45,7 @@ from plane.db.models import (
Project,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseViewSet
@@ -229,6 +230,10 @@ class IssueDraftViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
@@ -31,6 +31,7 @@ from plane.db.models import (
IssueAttachment,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.user_timezone_converter import user_timezone_converter
from collections import defaultdict
@@ -132,6 +133,10 @@ class SubIssuesEndpoint(BaseAPIView):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
sub_issues = user_timezone_converter(
sub_issues, datetime_fields, request.user.user_timezone
)
return Response(
{
"sub_issues": sub_issues,
@@ -32,6 +32,8 @@ from plane.db.models import (
ModuleLink,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseAPIView
@@ -199,6 +201,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
"updated_at",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
modules = user_timezone_converter(
modules, datetime_fields, request.user.user_timezone
)
return Response(modules, status=status.HTTP_200_OK)
else:
queryset = (
+45 -7
View File
@@ -1,6 +1,7 @@
# Python imports
import json
# Django Imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import (
@@ -17,14 +18,14 @@ from django.db.models import (
Value,
)
from django.db.models.functions import Coalesce
# Django Imports
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
from rest_framework import status
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
@@ -48,12 +49,12 @@ from plane.db.models import (
Project,
)
from plane.utils.analytics_plot import burndown_plot
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
from plane.utils.user_timezone_converter import user_timezone_converter
from plane.bgtasks.webhook_task import model_activity
from .. import BaseAPIView, BaseViewSet
class ModuleViewSet(WebhookMixin, BaseViewSet):
class ModuleViewSet(BaseViewSet):
model = Module
permission_classes = [
ProjectEntityPermission,
@@ -236,6 +237,20 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"updated_at",
)
).first()
# Send the model activity
model_activity.delay(
model_name="module",
model_id=str(module["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
datetime_fields = ["created_at", "updated_at"]
module = user_timezone_converter(
module, datetime_fields, request.user.user_timezone
)
return Response(module, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -277,6 +292,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"created_at",
"updated_at",
)
datetime_fields = ["created_at", "updated_at"]
modules = user_timezone_converter(
modules, datetime_fields, request.user.user_timezone
)
return Response(modules, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk):
@@ -418,6 +437,9 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
{"error": "Archived module cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
current_instance = json.dumps(
ModuleSerializer(module.first()).data, cls=DjangoJSONEncoder
)
serializer = ModuleWriteSerializer(
module.first(), data=request.data, partial=True
)
@@ -454,6 +476,22 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
"created_at",
"updated_at",
).first()
# Send the model activity
model_activity.delay(
model_name="module",
model_id=str(module["id"]),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
datetime_fields = ["created_at", "updated_at"]
module = user_timezone_converter(
module, datetime_fields, request.user.user_timezone
)
return Response(module, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+8 -3
View File
@@ -16,7 +16,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet, WebhookMixin
from .. import BaseViewSet
from plane.app.serializers import (
ModuleIssueSerializer,
IssueSerializer,
@@ -31,9 +31,9 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
class ModuleIssueViewSet(BaseViewSet):
serializer_class = ModuleIssueSerializer
model = ModuleIssue
webhook_event = "module_issue"
@@ -150,6 +150,11 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
# create multiple issues inside a module
+45 -8
View File
@@ -1,5 +1,6 @@
# Python imports
import boto3
import json
# Django imports
from django.db import IntegrityError
@@ -14,6 +15,7 @@ from django.db.models import (
)
from django.conf import settings
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
from rest_framework.response import Response
@@ -22,7 +24,7 @@ from rest_framework import serializers
from rest_framework.permissions import AllowAny
# Module imports
from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.views.base import BaseViewSet, BaseAPIView
from plane.app.serializers import (
ProjectSerializer,
ProjectListSerializer,
@@ -50,9 +52,10 @@ from plane.db.models import (
Issue,
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity
class ProjectViewSet(WebhookMixin, BaseViewSet):
class ProjectViewSet(BaseViewSet):
serializer_class = ProjectListSerializer
model = Project
webhook_event = "project"
@@ -193,6 +196,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk"),
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -211,6 +215,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
archived_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
archived_at__isnull=False,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -229,6 +234,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
draft_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
is_draft=True,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -331,6 +337,17 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
.filter(pk=serializer.data["id"])
.first()
)
model_activity.delay(
model_name="project",
model_id=str(project.id),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
serializer = ProjectListSerializer(project)
return Response(
serializer.data, status=status.HTTP_201_CREATED
@@ -361,7 +378,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=pk)
current_instance = json.dumps(
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
if project.archived_at:
return Response(
{"error": "Archived projects cannot be updated"},
@@ -399,6 +418,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
.filter(pk=serializer.data["id"])
.first()
)
model_activity.delay(
model_name="project",
model_id=str(project.id),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
serializer = ProjectListSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
@@ -573,11 +602,19 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
@cache_response(60 * 60 * 24, user=False)
def get(self, request):
files = []
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
if settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
else:
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
params = {
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/",
+10
View File
@@ -1,5 +1,7 @@
# Django imports
from django.db.models import Case, Count, IntegerField, Q, When
from django.contrib.auth import logout
from django.utils import timezone
# Third party imports
from rest_framework import status
@@ -26,6 +28,7 @@ from plane.db.models import (
from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.paginator import BasePaginator
from plane.authentication.utils.host import user_ip
class UserEndpoint(BaseViewSet):
@@ -166,7 +169,14 @@ class UserEndpoint(BaseViewSet):
"workspace_invite": False,
}
profile.save()
# User log out
user.last_logout_ip = user_ip(request=request)
user.last_logout_time = timezone.now()
user.save()
# Logout the user
logout(request)
return Response(status=status.HTTP_204_NO_CONTENT)

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