Compare commits

...
Author SHA1 Message Date
pablohashescobar 19b04d3073 dev: get issue_types 2024-07-09 17:35:08 +05:30
pablohashescobar cafa80e08e dev: add start date and target date to project 2024-07-09 13:29:51 +05:30
pablohashescobar 013cba8e29 Merge branch 'preview' of github.com:makeplane/plane into feat-issue-type-page-versioning 2024-07-09 13:18:05 +05:30
988201d729 [WEB - 1888] dev: log issue activity when transferring issues from one cycle to another (#5073)
* fix: cycle transfer activity

* chore: external api transfer issue

* chore: moved the cycle id

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-07-08 20:54:10 +05:30
Anmol Singh BhatiaandGitHub 6c2b28df91 fix: attachment delete modal (#5080) 2024-07-08 20:53:35 +05:30
Anmol Singh BhatiaandGitHub 53e5d4b40c [WEB-1680] dev: issue detail activity revamp and issue detail page improvement (#5075)
* chore: issue link activity message updated

* chore: activity filter type constant added

* dev: issue activity revamp and code refactor

* chore: issue detail widget oreder updated in peek overview

* chore: issue detail page padding improvement

* fix: relation widget toast alert

* fix: relation widget toast alert

* fix: peek overview attachment delete modal

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: issue detail sidebar parent field
2024-07-08 19:33:19 +05:30
Anmol Singh BhatiaandGitHub fd61079c8b chore: project active cycle progress state group color updated (#5077) 2024-07-08 18:52:47 +05:30
guru_sainathandGitHub 7767be2e21 [WEB-1889] fix: handled tapping on a notification in Notifications from mobile in inbox issue and issue peek overview component (#5074)
* fix: handled tapping on a notification in Notifications from mobile in inbox issue and issue peekoverview component

* fix: code cleanup

* fix: code cleanup on workspace notification store

* fix: updated selected notification on workspace notification store
2024-07-08 18:52:30 +05:30
rahulrameshaandGitHub a623456e63 [WEB-1890] fix: issue creation by using appropriate stores (#5072)
* fix issue creation by using appropriate stores

* add comments

* change useIssuesStore hook to use useIssueStoreType
2024-07-08 18:52:10 +05:30
Anmol Singh BhatiaandGitHub fb586c58d2 fix: created by tooltip removed (#5076) 2024-07-08 17:43:20 +05:30
Anmol Singh BhatiaandGitHub fb46249ccf [WEB-1872] fix: completed cycle gantt layout quick add issue validation (#5066)
* fix: completed cycle gantt layout quick add issue validation

* chore: code refactor
2024-07-08 15:55:53 +05:30
Anmol Singh BhatiaandGitHub 0e4ce2baa5 chore: issue widgets added to issue peek overview (#5069) 2024-07-08 15:51:59 +05:30
Anmol Singh BhatiaandGitHub 4e815c0fed fix: issue link error toast alert (#5068) 2024-07-08 15:28:38 +05:30
NikhilandGitHub 1cd55cd95b fix: remove user workspace cache on account deactivation (#5065) 2024-07-08 15:27:45 +05:30
d8d476463b [WEB-1728] Chore: Preload apis required to bootstrap the application (#5026)
* chore: prefetch apis

* chore: implemented cache-control

* Preload links with credentials

* chore: updated time in the cache and handled it based on cookie

* chore: make cache private

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-07-08 15:26:52 +05:30
guru_sainathandGitHub fc2585bf64 [WEB-1878] ui: implementing popover component in a UI package (#5063)
* ui: impleented popover commponent

* chore: implemente component in project-states

* chore: added default styling for popover menu panel

* chore: removed propsWithChildren in popover component type
2024-07-08 15:03:22 +05:30
pablohashescobar efcd96ee8c Merge branch 'preview' of github.com:makeplane/plane into feat-issue-type-page-versioning 2024-07-08 13:00:50 +05:30
Anmol Singh BhatiaandGitHub 6dcbea6d14 fix: module detail sidebar scroll (#5064) 2024-07-08 12:59:57 +05:30
Anmol Singh BhatiaandGitHub 12c24ad255 fix: dashboard collaborators endpoint active member filter (#5062) 2024-07-08 12:04:00 +05:30
Anmol Singh BhatiaandGitHub 387dbd89f5 [WEB-1679] feat: issue detail widgets (#5034)
* chore: issue detail sidebar and main content improvement and code refactor

* dev: issue relation list component added

* chore: code refactor

* dev: issue detail widget implementation

* dev: update issue relation endpoint to return same response as sub issue

* chore: changed updated_by in issue attachment

* fix: peek view link ui

* chore: move collapsible button component to plane ui package

* chore: issue list component code refactor

* chore: relation icon updated

* chore: relation icon updated

* chore: issue quick action ui updated

* chore: wrap title indicatorElement component with useMemo

* chore: code refactor

* fix: build error
2024-07-05 16:51:58 +05:30
Manish GuptaandGitHub b7d792ed07 fix: Fixed aio build (#5056)
* fix: aio build

* fix aio branch build
2024-07-05 16:45:02 +05:30
pablohashescobar 4609adf6cc dev: add is_default value in issue type 2024-07-05 16:38:06 +05:30
54a5e5e761 [WEB-1437] feat: notifications mention filter (#5040)
* chore: implemented mentions on the notification

* chore: mention notification filter

* chore: handled mentions refetch and total count on header and sidebar menu option

* chore: seperated notifications empty state

* chore: updated sidebar menu option notification vaidation

* chore: handled notificaition sidebar total notifications count

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-07-05 16:13:09 +05:30
Anmol Singh BhatiaandGitHub 837f09ed90 fix: kanban subgroup quick add validation (#5055) 2024-07-05 16:10:16 +05:30
guru_sainathandGitHub 38f8aa90c1 [WEB-1519] chore: update component structure in project state settings and implement DND (#5043)
* chore: updated project settings state

* chore: updated sorting on project state

* chore: updated grab handler in state item

* chore: Updated UI and added garb handler icon

* chore: handled top and bottom sequence in middle element swap

* chore: handled input state element char limit to 100

* chore: typos and code cleanup in create state

* chore: handled typos and comments wherever is required

* chore: handled sorting logic
2024-07-05 16:09:33 +05:30
guru_sainathandGitHub c75091ca3a [WEB-1803] chore: workspace notification sorting when we refresh the notifications on workspace notifications (#5049)
* chore: workspace notification sorting when we refresh the notifications on workspace notifications

* chore: replaced sorting with ISO to EPOCH

* chore: converted obj to array
2024-07-05 16:09:16 +05:30
61ce055cb3 [WEB - 1740] chore: add issue id in pages detail endpoint (#4942)
* chore: add issue id in pages detail endpoint

* fix: response structure changed

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-07-05 15:00:15 +05:30
Prateek ShouryaandGitHub 977b47d35f [WEB-1843] chore: minor file restructuring. (#5044) 2024-07-05 14:58:42 +05:30
Bavisetti NarayanandGitHub 509c258b07 chore: custom analytics charts (#5052) 2024-07-05 14:55:48 +05:30
Vihar KuramaandGitHub 3cfb0ac518 update assignes for github issue templates (#5053) 2024-07-05 14:30:37 +05:30
guru_sainathandGitHub 156f1011f3 chore: Active cycle muatation when current cycle creation when current_date is in between start_date and end_date (#5050) 2024-07-05 12:13:28 +05:30
pablohashescobar 5f1cc70938 Merge branch 'preview' of github.com:makeplane/plane into feat-issue-type-page-versioning 2024-07-05 11:26:16 +05:30
Bavisetti NarayanandGitHub a36d1a753e chore: corrected the subissue ordering (#5030) 2024-07-04 19:51:05 +05:30
Bavisetti NarayanandGitHub 9a927ded84 chore: estimate points float field (#5038) 2024-07-04 19:50:08 +05:30
rahulrameshaandGitHub 72f00e378d fix build errors due to conflicting PRs (#5047) 2024-07-04 17:13:04 +05:30
guru_sainathandGitHub d3ec1aa422 [WEB-1792] chore: updated UI notification tooltip on header and app sidebar (#5046)
* ui: updatedm UI notificaton tooltip on header and app sidebar

* fix: reverted notification sorting

* fix: renamed function name updateIssue with issueUpdate in issue base helper file
2024-07-04 17:04:00 +05:30
Anmol Singh BhatiaandGitHub dda19b0a3f fix: issue peek view link list ui (#5045) 2024-07-04 17:03:25 +05:30
Aaryan KhandelwalandGitHub 2b570da890 chore: change comment box placeholder (#5042) 2024-07-04 16:21:05 +05:30
rahulrameshaandGitHub 5918607171 [WEB-1818] fix: issue changes done in Peek overview to reflect in the issue list boards (#5010)
* fix issue changes done in Peek overview to reflect in the issue boards

* Adding comments to aliased names
2024-07-04 16:18:33 +05:30
rahulrameshaandGitHub f1496e3144 fix module Quick add (#5039) 2024-07-04 16:17:49 +05:30
sriram veeraghanta 9717497b4e chore: package version change 2024-07-04 15:27:37 +05:30
guru_sainathandGitHub 2f8c8ac40f [WEB-1847] chore: handled project id on the issue creation modal toast when the issue creation is happening on the home page (#5033)
* chore: handled project id on the issue creation modal toast when the issue creation is happening on the home page

* chore: updated issue modal condition
2024-07-04 12:08:26 +05:30
guru_sainathandGitHub 734e920e08 chore: updated dropdown hover ui on estimate dropdown menu options (#5032) 2024-07-03 19:17:01 +05:30
Prateek ShouryaandGitHub 82fa1347d1 [WEB-1843] chore: active cycles file restructuring. (#5031) 2024-07-03 19:14:15 +05:30
rahulrameshaandGitHub a7aa5c2ba7 [WEB-1829] fix: stop issues query before fetching the view filters (#5015)
* fix to not query issues before fetching the view filters

* add default group by for Kanban views
2024-07-03 16:25:41 +05:30
NikhilandGitHub 825b2f26bf fix: issue pagination listing (#5029) 2024-07-03 15:56:12 +05:30
Anmol Singh BhatiaandGitHub af51992eba [WEB-1715] chore: issue filters indicator enhancement (#5027)
* chore: isIssueFilterActive helper function added

* chore: isIssueFilterActive implementation

* chore: code refactor
2024-07-03 15:43:22 +05:30
Anmol Singh BhatiaandGitHub 8f59a36bda fix: sidebar quick action overlapping (#5020) 2024-07-03 13:49:28 +05:30
Manish GuptaandGitHub 5ad0114aac fix:feature preview (#4976)
* custom base tags

* fixed aio actions
2024-07-03 13:30:25 +05:30
NikhilandGitHub 095639b976 fix: workspace slug validation (#5023) 2024-07-03 13:19:57 +05:30
Prateek ShouryaandGitHub db722d580f [WEB-1801] fix: avoid opening shortcut guide on editors / input elements. (#5025) 2024-07-03 13:11:51 +05:30
Anmol Singh BhatiaandGitHub 0363057d9c fix: modal core onClose (#5018) 2024-07-03 11:34:52 +05:30
pablohashescobar 67cc0365bc Merge branch 'preview' of github.com:makeplane/plane into feat-issue-type-page-versioning 2024-07-02 20:15:17 +05:30
NikhilandGitHub 3dc933f0e8 [WEB - 1835] chore: update workspace constants (#5017)
* dev: add workspace restriction list in constants

* dev: update list
2024-07-02 20:05:35 +05:30
guru_sainathandGitHub fc33238d89 fix: updated unread count UI and validation (#5016) 2024-07-02 19:43:22 +05:30
Aaryan KhandelwalandGitHub 86464c1d6f fix: project publish redirection (#5004) 2024-07-02 19:28:55 +05:30
Prateek ShouryaandGitHub cc479f39a7 [WEB-1808] style: fix settings highlight on app sidebar. (#4995) 2024-07-02 19:28:02 +05:30
Aaryan KhandelwalandGitHub b50df9ef99 fix: peek overview layout dropdown icons (#4993) 2024-07-02 19:27:13 +05:30
rahulrameshaandGitHub c637639a3e tweak pagination and virtualization thresholds to have a smoother scroll (#4991) 2024-07-02 19:26:26 +05:30
M. PalanikannanandGitHub 12401c54cc fix: update link view now updates the link (#4989) 2024-07-02 19:25:32 +05:30
Anmol Singh BhatiaandGitHub 1201a4245e [WEB-1679] chore: sub-issues, attachments, and links UI revamp (#5007)
* chore: issue attachment ui revamp

* chore: issue link ui revamp

* chore: attachment icon improvement

* chore: sub-issue ui revamp

* chore: open on hover functionality added to custom menu

* chore: code refactor
2024-07-02 19:06:20 +05:30
guru_sainathandGitHub fc15ca5565 [WEB-1792] fix: handled redirection issue when we change the status of the inbox issue in the notification (#5014)
* fix: handled redirection issue when we change the status of inbox issue in notification

* fix: updated condition
2024-07-02 18:49:40 +05:30
QuadruboandGitHub 4e8b7e6dbb fix: api requests cors (#4929) 2024-07-02 18:09:14 +05:30
Anmol Singh BhatiaandGitHub b0bc818362 [WEB-1819] dev: collapsible component (#5001)
* dev: accordion component added

* chore: code refactor

* chore: collapsible component improvement
2024-07-02 17:10:53 +05:30
c8491a13b3 [WEB - 1827]remove: migration for account and social login connection (#5013)
* fix: OAuth adapter error codes + missing account provider migration that introduces GitLab (#4998)

* feat(apiserver): GitLab OAuth client

* feat(admin,packages,space,web): GitLab OAuth client

* Feat(apiserver/oauth): authentication_error_code()

* chore: remove empty files introduced by rebase

* dev: delete migration

---------

Co-authored-by: jon ⚝ <jon@allmende.io>
2024-07-02 17:06:22 +05:30
guru_sainathandGitHub 83587c2c6b fix: converted and handled the estimate type to lowercase in the store (#5011) 2024-07-02 16:35:06 +05:30
rahulrameshaandGitHub d9d62c2d5a Add empty state when view is not available (#5002) 2024-07-02 16:21:28 +05:30
Anmol Singh BhatiaandGitHub b591203da6 [WEB-1679] chore: relation and attachment icons (#5005)
* chore: attachment icon improvement

* chore: relation and dropdown icon added

* chore: code refactor
2024-07-02 16:15:42 +05:30
Prateek ShouryaandGitHub 78e0405971 [WEB-1801] improvement: open shortcut guide using shift+/ key combination. (#5000) 2024-07-02 16:14:42 +05:30
26040144fc [WEB-1792] chore: integrated inbox issue in notification peek view and handled increment/decrement of unread notifications (#5008)
* chore: added a boolean field in notification list

* chore: notification filters changed

* chore: handled inbox notification and typo on the card items

* chore: handled notification count increment and decrement

* chore: typos and ui updates

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-07-02 16:12:27 +05:30
Prateek ShouryaandGitHub 0fd36257d7 [WEB-1814] chore: update OIDC and SAML logo. (#5009)
* [WEB-1814] chore: update OIDC and SAML logo.

* chore: update OIDC logo.
2024-07-02 16:11:57 +05:30
Anmol Singh BhatiaandGitHub c217185b07 chore: open on hover functionality added to custom menu (#5003) 2024-07-02 14:53:17 +05:30
Anmol Singh BhatiaandGitHub a5628c4ce1 fix: issue link count mutation (#5006) 2024-07-02 14:53:05 +05:30
pablohashescobar 01ec930a25 dev: create page version endpoints 2024-07-01 20:03:09 +05:30
pablohashescobar 9f59aea470 dev: add page versioning migrations 2024-07-01 14:50:15 +05:30
pablohashescobar c097cb9581 dev: create page version 2024-07-01 13:49:17 +05:30
pablohashescobar 8824d21405 dev: fix migration for issue types 2024-07-01 11:46:46 +05:30
pablohashescobar b6b282bb2a dev: fix save 2024-06-30 20:08:48 +05:30
pablohashescobar 791ddfcf13 dev: create issue types and add back migration for existing issues 2024-06-30 19:56:20 +05:30
261 changed files with 6160 additions and 2180 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Bug report
description: Create a bug report to help us improve Plane
title: "[bug]: "
labels: [🐛bug]
assignees: [srinivaspendem, pushya22]
assignees: [vihar, pushya22]
body:
- type: markdown
attributes:
@@ -2,7 +2,7 @@ name: Feature request
description: Suggest a feature to improve Plane
title: "[feature]: "
labels: [✨feature]
assignees: [srinivaspendem, pushya22]
assignees: [vihar, pushya22]
body:
- type: markdown
attributes:
+18 -13
View File
@@ -2,6 +2,11 @@ name: Build AIO Base Image
on:
workflow_dispatch:
inputs:
base_tag_name:
description: 'Base Tag Name'
required: false
default: ''
env:
TARGET_BRANCH: ${{ github.ref_name }}
@@ -22,27 +27,29 @@ jobs:
- id: set_env_variables
name: Set Environment Variables
run: |
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
echo "IMAGE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
echo "IMAGE_TAG=preview" >> $GITHUB_OUTPUT
else
echo "IMAGE_TAG=develop" >> $GITHUB_OUTPUT
fi
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=makeplane/plane-dev" >> $GITHUB_OUTPUT
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT
else
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
if [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
echo "TARGET_BRANCH=preview" >> $GITHUB_OUTPUT
echo "IMAGE_TAG=preview" >> $GITHUB_OUTPUT
else
echo "TARGET_BRANCH=develop" >> $GITHUB_OUTPUT
echo "IMAGE_TAG=develop" >> $GITHUB_OUTPUT
fi
fi
- id: checkout_files
@@ -54,7 +61,6 @@ jobs:
needs: [base_build_setup]
env:
BASE_IMG_TAG: makeplane/plane-aio-base:full-${{ needs.base_build_setup.outputs.image_tag }}
TARGET_BRANCH: ${{ needs.base_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.base_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.base_build_setup.outputs.gh_buildx_version }}
BUILDX_PLATFORMS: ${{ needs.base_build_setup.outputs.gh_buildx_platforms }}
@@ -96,7 +102,6 @@ jobs:
needs: [base_build_setup]
env:
BASE_IMG_TAG: makeplane/plane-aio-base:slim-${{ needs.base_build_setup.outputs.image_tag }}
TARGET_BRANCH: ${{ needs.base_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.base_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.base_build_setup.outputs.gh_buildx_version }}
BUILDX_PLATFORMS: ${{ needs.base_build_setup.outputs.gh_buildx_platforms }}
+15 -5
View File
@@ -13,6 +13,10 @@ on:
type: boolean
required: false
default: false
base_tag_name:
description: 'Base Tag Name'
required: false
default: ''
release:
types: [released, prereleased]
@@ -27,6 +31,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
flat_branch_name: ${{ steps.set_env_variables.outputs.FLAT_BRANCH_NAME }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
@@ -52,7 +57,9 @@ jobs:
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
if [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
if [ "${{ github.event_name}}" == "workflow_dispatch" ] && [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
echo "AIO_BASE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
echo "AIO_BASE_TAG=preview" >> $GITHUB_OUTPUT
else
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
@@ -72,6 +79,9 @@ jobs:
echo "DO_SLIM_BUILD=false" >> $GITHUB_OUTPUT
fi
FLAT_BRANCH_NAME=$(echo "${{ env.TARGET_BRANCH }}" | sed 's/[^a-zA-Z0-9]/-/g')
echo "FLAT_BRANCH_NAME=$FLAT_BRANCH_NAME" >> $GITHUB_OUTPUT
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
@@ -83,7 +93,7 @@ jobs:
env:
BUILD_TYPE: full
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
AIO_IMAGE_TAGS: makeplane/plane-aio:full-${{ needs.branch_build_setup.outputs.gh_branch_name }}
AIO_IMAGE_TAGS: makeplane/plane-aio:full-${{ needs.branch_build_setup.outputs.flat_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -126,7 +136,7 @@ jobs:
tags: ${{ env.AIO_IMAGE_TAGS }}
push: true
build-args: |
BUILD_TAG=${{ env.AIO_BASE_TAG }}
BASE_TAG=${{ env.AIO_BASE_TAG }}
BUILD_TYPE=${{env.BUILD_TYPE}}
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -143,7 +153,7 @@ jobs:
env:
BUILD_TYPE: slim
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
AIO_IMAGE_TAGS: makeplane/plane-aio:slim-${{ needs.branch_build_setup.outputs.gh_branch_name }}
AIO_IMAGE_TAGS: makeplane/plane-aio:slim-${{ needs.branch_build_setup.outputs.flat_branch_name }}
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
@@ -186,7 +196,7 @@ jobs:
tags: ${{ env.AIO_IMAGE_TAGS }}
push: true
build-args: |
BUILD_TAG=${{ env.AIO_BASE_TAG }}
BASE_TAG=${{ env.AIO_BASE_TAG }}
BUILD_TYPE=${{env.BUILD_TYPE}}
cache-from: type=gha
cache-to: type=gha,mode=max
+11 -1
View File
@@ -2,6 +2,11 @@ name: Feature Preview
on:
workflow_dispatch:
inputs:
base_tag_name:
description: 'Base Tag Name'
required: false
default: 'preview'
env:
TARGET_BRANCH: ${{ github.ref_name }}
@@ -29,7 +34,12 @@ jobs:
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
echo "AIO_BASE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
else
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
fi
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
@@ -13,7 +13,7 @@ import { AuthenticationMethodCard } from "@/components/authentication";
import { UpgradeButton } from "@/components/common/upgrade-button";
import { getBaseAuthenticationModes } from "@/helpers/authentication.helper";
// images
import OIDCLogo from "@/public/logos/oidc-logo.png";
import OIDCLogo from "@/public/logos/oidc-logo.svg";
import SAMLLogo from "@/public/logos/saml-logo.svg";
export type TAuthenticationModeProps = {
@@ -32,7 +32,7 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
key: "oidc",
name: "OIDC",
description: "Authenticate your users via the OpenID Connect protocol.",
icon: <Image src={OIDCLogo} height={20} width={20} alt="OIDC Logo" />,
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
config: <UpgradeButton />,
unavailable: true,
},
@@ -40,7 +40,7 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
key: "saml",
name: "SAML",
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
icon: <Image src={SAMLLogo} height={24} width={24} alt="SAML Logo" className="pb-0.5 pl-0.5" />,
icon: <Image src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
config: <UpgradeButton />,
unavailable: true,
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.21.0",
"version": "0.22.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg width="92" height="84" viewBox="0 0 92 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3695_11896)">
<path d="M83.0398 32.6876C74.2901 27.2397 62.0735 23.8553 48.7013 23.8553C21.7918 23.8553 0 37.3101 0 53.9016C0 69.0898 18.1598 81.554 41.685 83.7001V74.9504C25.8364 72.9693 13.95 64.3022 13.95 53.9016C13.95 42.0977 29.4684 32.44 48.7013 32.44C58.2765 32.44 66.9436 34.8338 73.217 38.7134L64.3022 44.2439H92.1197V27.0746L83.0398 32.6876Z" fill="#CCCCCC"/>
<path d="M41.6846 8.99736V74.9504V83.7002L55.6346 74.9504V0L41.6846 8.99736Z" fill="#FF6200"/>
</g>
<defs>
<clipPath id="clip0_3695_11896">
<rect width="92" height="84" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 702 B

+16 -15
View File
@@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
<g fill="#C22E33">
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
<svg width="700" height="650" viewBox="0 0 700 650" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3262_5767)">
<mask id="mask0_3262_5767" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="700" height="650">
<path d="M700 0H0V650H700V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_3262_5767)">
<path d="M337.682 0L360.832 20.5C377.982 35.7 395.182 50.85 412.132 66.25C521.982 166 614.982 278.25 684.982 407.45C688.582 414.05 691.832 420.85 695.082 427.6L699.982 437.75L694.582 440.6L690.532 434.85L680.032 419.9L672.682 409.2C621.732 335.25 570.682 261.2 500.582 201.95C479.373 183.995 455.969 168.807 430.932 156.75C380.232 132.5 335.132 142.2 296.432 182C259.632 219.85 240.532 266.85 223.282 314.65C221.032 320.8 218.682 326.9 216.332 333L212.232 343.75L203.632 341C208.632 323.6 213.232 306.1 217.832 288.55C228.332 248.8 238.832 209.05 253.432 170.75C268.932 129.95 288.532 90.6 308.082 51.25C316.532 34.2 324.982 17.15 333.082 0H337.682Z" fill="#C22E33"/>
<path d="M372.382 491.1C291.082 529.6 94.3829 569.3 1.08287 559.1C-14.1671 478.8 135.482 102.5 208.982 45.5L204.232 56.4C202.115 61.531 199.813 66.5842 197.332 71.55L194.032 78C156.032 151.1 118.082 224.3 98.6329 304.5C91.6287 332.124 87.8038 360.458 87.2328 388.95C86.7328 455.95 128.432 501.55 198.082 504.4C231.582 505.75 265.432 502.25 299.232 498.7C313.932 497.2 328.582 495.65 343.232 494.5C348.632 494.1 353.932 493.45 360.832 492.55L372.382 491.15V491.1Z" fill="#C22E33"/>
<path d="M141.233 639.05C118.983 640.75 96.733 642.45 74.583 644.45C279.433 663.95 476.083 630.6 670.083 562.25C606.833 450.75 521.583 362.7 422.483 286.15C423.783 291.05 426.683 294.6 429.533 298.1L431.933 301.1C440.433 312.4 449.333 323.5 458.283 334.6C478.733 360.05 499.183 385.5 514.583 413.5C553.483 484.5 532.383 545.9 456.183 578.3C406.083 599.65 351.333 614.2 297.183 622.9C245.683 631.1 193.433 635.05 141.233 639.05Z" fill="#C22E33"/>
</g>
</svg>
</g>
<defs>
<clipPath id="clip0_3262_5767">
<rect width="700" height="650" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.21.0"
"version": "0.22.0"
}
+3 -3
View File
@@ -13,9 +13,9 @@ class CycleSerializer(BaseSerializer):
started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True)
total_estimates = serializers.FloatField(read_only=True)
completed_estimates = serializers.FloatField(read_only=True)
started_estimates = serializers.FloatField(read_only=True)
def validate(self, data):
if (
+15 -5
View File
@@ -11,6 +11,7 @@ from rest_framework import serializers
# Module imports
from plane.db.models import (
Issue,
IssueType,
IssueActivity,
IssueAssignee,
IssueAttachment,
@@ -131,7 +132,12 @@ class IssueSerializer(BaseSerializer):
workspace_id = self.context["workspace_id"]
default_assignee_id = self.context["default_assignee_id"]
issue = Issue.objects.create(**validated_data, project_id=project_id)
# Get the issue type from the project
issue_type = IssueType.objects.filter(project_id=project_id).first()
issue = Issue.objects.create(
**validated_data, project_id=project_id, type=issue_type
)
# Issue Audit Users
created_by_id = issue.created_by_id
@@ -312,10 +318,14 @@ class IssueLinkSerializer(BaseSerializer):
return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
).exclude(pk=instance.id).exists():
if (
IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
+33 -7
View File
@@ -12,7 +12,7 @@ from django.db.models import (
OuterRef,
Q,
Sum,
IntegerField,
FloatField,
)
from django.db.models.functions import Cast
@@ -867,12 +867,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("display_name", "assignee_id", "avatar")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -882,7 +882,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -921,12 +921,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -936,7 +936,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -1140,12 +1140,38 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
updated_cycles = []
update_cycle_issue_activity = []
for cycle_issue in cycle_issues:
cycle_issue.cycle_id = new_cycle_id
updated_cycles.append(cycle_issue)
update_cycle_issue_activity.append(
{
"old_cycle_id": str(cycle_id),
"new_cycle_id": str(new_cycle_id),
"issue_id": str(cycle_issue.issue_id),
}
)
cycle_issues = CycleIssue.objects.bulk_update(
updated_cycles, ["cycle_id"], batch_size=100
)
# Capture Issue Activity
issue_activity.delay(
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": []}),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": "[]",
}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response({"message": "Success"}, status=status.HTTP_200_OK)
+5 -1
View File
@@ -16,6 +16,7 @@ from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import (
Inbox,
IssueType,
InboxIssue,
Issue,
Project,
@@ -141,6 +142,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
color="#ff7700",
is_triage=True,
)
# Get the issue type
issue_type = IssueType.objects.filter(project_id=project_id).first()
# create an issue
issue = Issue.objects.create(
@@ -149,9 +152,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
description_html=request.data.get("issue", {}).get(
"description_html", "<p></p>"
),
priority=request.data.get("issue", {}).get("priority", "low"),
priority=request.data.get("issue", {}).get("priority", "none"),
project_id=project_id,
state=state,
type=issue_type,
)
# create an inbox issue
+9
View File
@@ -26,6 +26,7 @@ from plane.db.models import (
ProjectMember,
State,
Workspace,
IssueType,
)
from plane.bgtasks.webhook_task import model_activity
from .base import BaseAPIView
@@ -240,6 +241,14 @@ class ProjectAPIEndpoint(BaseAPIView):
.filter(pk=serializer.data["id"])
.first()
)
# Create the Issue Types
IssueType.objects.create(
name="Task",
description="A task that needs to be done",
project_id=project.id,
)
# Model activity
model_activity.delay(
model_name="project",
@@ -91,6 +91,7 @@ from .page import (
PageLogSerializer,
SubPageSerializer,
PageDetailSerializer,
PageVersionSerializer,
)
from .estimate import (
+16 -6
View File
@@ -33,6 +33,7 @@ from plane.db.models import (
IssueVote,
IssueRelation,
State,
IssueType,
)
@@ -135,7 +136,12 @@ class IssueCreateSerializer(BaseSerializer):
workspace_id = self.context["workspace_id"]
default_assignee_id = self.context["default_assignee_id"]
issue = Issue.objects.create(**validated_data, project_id=project_id)
# Get Issue Type
issue_type = IssueType.objects.filter(project_id=project_id).first()
# Create Issue
issue = Issue.objects.create(
**validated_data, project_id=project_id, type=issue_type
)
# Issue Audit Users
created_by_id = issue.created_by_id
@@ -459,10 +465,14 @@ class IssueLinkSerializer(BaseSerializer):
return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
).exclude(pk=instance.id).exists():
if (
IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
@@ -509,7 +519,7 @@ class IssueAttachmentLiteSerializer(DynamicBaseSerializer):
"attributes",
"issue_id",
"updated_at",
"updated_by_id",
"updated_by",
]
read_only_fields = fields
+6 -6
View File
@@ -177,8 +177,8 @@ class ModuleSerializer(DynamicBaseSerializer):
started_issues = serializers.IntegerField(read_only=True)
unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True)
total_estimate_points = serializers.IntegerField(read_only=True)
completed_estimate_points = serializers.IntegerField(read_only=True)
total_estimate_points = serializers.FloatField(read_only=True)
completed_estimate_points = serializers.FloatField(read_only=True)
class Meta:
model = Module
@@ -222,10 +222,10 @@ class ModuleSerializer(DynamicBaseSerializer):
class ModuleDetailSerializer(ModuleSerializer):
link_module = ModuleLinkSerializer(read_only=True, many=True)
sub_issues = serializers.IntegerField(read_only=True)
backlog_estimate_points = serializers.IntegerField(read_only=True)
unstarted_estimate_points = serializers.IntegerField(read_only=True)
started_estimate_points = serializers.IntegerField(read_only=True)
cancelled_estimate_points = serializers.IntegerField(read_only=True)
backlog_estimate_points = serializers.FloatField(read_only=True)
unstarted_estimate_points = serializers.FloatField(read_only=True)
started_estimate_points = serializers.FloatField(read_only=True)
cancelled_estimate_points = serializers.FloatField(read_only=True)
class Meta(ModuleSerializer.Meta):
fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues", "backlog_estimate_points", "unstarted_estimate_points", "started_estimate_points", "cancelled_estimate_points"]
@@ -3,11 +3,15 @@ from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import Notification, UserNotificationPreference
# Third Party imports
from rest_framework import serializers
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(
read_only=True, source="triggered_by"
)
is_inbox_issue = serializers.BooleanField(read_only=True)
class Meta:
model = Notification
+11
View File
@@ -10,6 +10,7 @@ from plane.db.models import (
Label,
ProjectPage,
Project,
PageVersion,
)
@@ -161,3 +162,13 @@ class PageLogSerializer(BaseSerializer):
"workspace",
"page",
]
class PageVersionSerializer(BaseSerializer):
class Meta:
model = PageVersion
fields = "__all__"
read_only_fields = [
"workspace",
"page",
]
+6 -16
View File
@@ -15,6 +15,7 @@ from plane.db.models import (
WorkspaceTheme,
WorkspaceUserProperties,
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
class WorkSpaceSerializer(DynamicBaseSerializer):
@@ -22,22 +23,11 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
total_members = serializers.IntegerField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
def validated(self, data):
if data.get("slug") in [
"404",
"accounts",
"api",
"create-workspace",
"god-mode",
"installations",
"invitations",
"onboarding",
"profile",
"spaces",
"workspace-invitations",
"password",
]:
raise serializers.ValidationError({"slug": "Slug is not valid"})
def validate_slug(self, value):
# Check if the slug is restricted
if value in RESTRICTED_WORKSPACE_SLUGS:
raise serializers.ValidationError("Slug is not valid")
return value
class Meta:
model = Workspace
+11
View File
@@ -7,6 +7,7 @@ from plane.app.views import (
PageLogEndpoint,
SubPagesEndpoint,
PagesDescriptionViewSet,
PageVersionEndpoint,
)
@@ -90,4 +91,14 @@ urlpatterns = [
),
name="page-description",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/versions/",
PageVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/versions/<uuid:pk>/",
PageVersionEndpoint.as_view(),
name="page-versions",
),
]
+1
View File
@@ -179,6 +179,7 @@ from .page.base import (
SubPagesEndpoint,
PagesDescriptionViewSet,
)
from .page.version import PageVersionEndpoint
from .search.base import GlobalSearchEndpoint
from .search.issue import IssueSearchEndpoint
+65 -39
View File
@@ -19,7 +19,7 @@ from django.db.models import (
When,
Subquery,
Sum,
IntegerField,
FloatField,
)
from django.db.models.functions import Coalesce, Cast
from django.utils import timezone
@@ -86,7 +86,7 @@ class CycleViewSet(BaseViewSet):
.values("issue_cycle__cycle_id")
.annotate(
backlog_estimate_point=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("backlog_estimate_point")[:1]
@@ -100,7 +100,7 @@ class CycleViewSet(BaseViewSet):
.values("issue_cycle__cycle_id")
.annotate(
unstarted_estimate_point=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("unstarted_estimate_point")[:1]
@@ -114,7 +114,7 @@ class CycleViewSet(BaseViewSet):
.values("issue_cycle__cycle_id")
.annotate(
started_estimate_point=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("started_estimate_point")[:1]
@@ -128,7 +128,7 @@ class CycleViewSet(BaseViewSet):
.values("issue_cycle__cycle_id")
.annotate(
cancelled_estimate_point=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("cancelled_estimate_point")[:1]
@@ -142,7 +142,7 @@ class CycleViewSet(BaseViewSet):
.values("issue_cycle__cycle_id")
.annotate(
completed_estimate_points=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("completed_estimate_points")[:1]
@@ -155,7 +155,7 @@ class CycleViewSet(BaseViewSet):
.values("issue_cycle__cycle_id")
.annotate(
total_estimate_points=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("total_estimate_points")[:1]
@@ -287,37 +287,37 @@ class CycleViewSet(BaseViewSet):
.annotate(
backlog_estimate_points=Coalesce(
Subquery(backlog_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
unstarted_estimate_points=Coalesce(
Subquery(unstarted_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
started_estimate_points=Coalesce(
Subquery(started_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
cancelled_estimate_points=Coalesce(
Subquery(cancelled_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
completed_estimate_points=Coalesce(
Subquery(completed_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
total_estimate_points=Coalesce(
Subquery(total_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.order_by("-is_favorite", "name")
@@ -395,12 +395,12 @@ class CycleViewSet(BaseViewSet):
.values("display_name", "assignee_id", "avatar")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -410,7 +410,7 @@ class CycleViewSet(BaseViewSet):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -433,12 +433,12 @@ class CycleViewSet(BaseViewSet):
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -448,7 +448,7 @@ class CycleViewSet(BaseViewSet):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -465,14 +465,14 @@ class CycleViewSet(BaseViewSet):
}
if data[0]["start_date"] and data[0]["end_date"]:
data[0]["estimate_distribution"]["completion_chart"] = (
burndown_plot(
queryset=queryset.first(),
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=data[0]["id"],
)
data[0]["estimate_distribution"][
"completion_chart"
] = burndown_plot(
queryset=queryset.first(),
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=data[0]["id"],
)
assignee_distribution = (
@@ -836,12 +836,12 @@ class CycleViewSet(BaseViewSet):
.values("display_name", "assignee_id", "avatar")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -851,7 +851,7 @@ class CycleViewSet(BaseViewSet):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -874,12 +874,12 @@ class CycleViewSet(BaseViewSet):
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -889,7 +889,7 @@ class CycleViewSet(BaseViewSet):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -1234,12 +1234,12 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.values("display_name", "assignee_id", "avatar")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -1249,7 +1249,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -1288,12 +1288,12 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -1303,7 +1303,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -1507,14 +1507,40 @@ class TransferCycleIssueEndpoint(BaseAPIView):
)
updated_cycles = []
update_cycle_issue_activity = []
for cycle_issue in cycle_issues:
cycle_issue.cycle_id = new_cycle_id
updated_cycles.append(cycle_issue)
update_cycle_issue_activity.append(
{
"old_cycle_id": str(cycle_id),
"new_cycle_id": str(new_cycle_id),
"issue_id": str(cycle_issue.issue_id),
}
)
cycle_issues = CycleIssue.objects.bulk_update(
updated_cycles, ["cycle_id"], batch_size=100
)
# Capture Issue Activity
issue_activity.delay(
type="cycle.activity.created",
requested_data=json.dumps({"cycles_list": []}),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": "[]",
}
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response({"message": "Success"}, status=status.HTTP_200_OK)
@@ -542,6 +542,7 @@ def dashboard_recent_collaborators(self, request, slug):
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
is_active=True,
)
.annotate(
num_activities=Coalesce(
+134 -35
View File
@@ -3,8 +3,11 @@ import json
# Django imports
from django.utils import timezone
from django.db.models import Q
from django.db.models import Q, OuterRef, F, Func, UUIDField, Value, CharField
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models.functions import Coalesce
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
# Third Party imports
from rest_framework.response import Response
@@ -20,6 +23,9 @@ from plane.app.permissions import ProjectEntityPermission
from plane.db.models import (
Project,
IssueRelation,
Issue,
IssueAttachment,
IssueLink,
)
from plane.bgtasks.issue_activites_task import issue_activity
@@ -61,56 +67,149 @@ class IssueRelationViewSet(BaseViewSet):
.order_by("-created_at")
.distinct()
)
# get all blocking issues
blocking_issues = issue_relations.filter(
relation_type="blocked_by", related_issue_id=issue_id
)
).values_list("issue_id", flat=True)
# get all blocked by issues
blocked_by_issues = issue_relations.filter(
relation_type="blocked_by", issue_id=issue_id
)
).values_list("related_issue_id", flat=True)
# get all duplicate issues
duplicate_issues = issue_relations.filter(
issue_id=issue_id, relation_type="duplicate"
)
).values_list("related_issue_id", flat=True)
# get all relates to issues
duplicate_issues_related = issue_relations.filter(
related_issue_id=issue_id, relation_type="duplicate"
)
).values_list("issue_id", flat=True)
# get all relates to issues
relates_to_issues = issue_relations.filter(
issue_id=issue_id, relation_type="relates_to"
)
).values_list("related_issue_id", flat=True)
# get all relates to issues
relates_to_issues_related = issue_relations.filter(
related_issue_id=issue_id, relation_type="relates_to"
)
).values_list("issue_id", flat=True)
blocked_by_issues_serialized = IssueRelationSerializer(
blocked_by_issues, many=True
).data
duplicate_issues_serialized = IssueRelationSerializer(
duplicate_issues, many=True
).data
relates_to_issues_serialized = IssueRelationSerializer(
relates_to_issues, many=True
).data
queryset = (
Issue.issue_objects.filter(
workspace__slug=slug,
project_id=project_id,
)
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.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)
& Q(assignees__member_project__is_active=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
).distinct()
# revere relation for blocked by issues
blocking_issues_serialized = RelatedIssueSerializer(
blocking_issues, many=True
).data
# reverse relation for duplicate issues
duplicate_issues_related_serialized = RelatedIssueSerializer(
duplicate_issues_related, many=True
).data
# reverse relation for related issues
relates_to_issues_related_serialized = RelatedIssueSerializer(
relates_to_issues_related, many=True
).data
# Fields
fields = [
"id",
"name",
"state_id",
"sort_order",
"priority",
"sequence_id",
"project_id",
"label_ids",
"assignee_ids",
"created_at",
"updated_at",
"created_by",
"updated_by",
"relation_type",
]
response_data = {
"blocking": blocking_issues_serialized,
"blocked_by": blocked_by_issues_serialized,
"duplicate": duplicate_issues_serialized
+ duplicate_issues_related_serialized,
"relates_to": relates_to_issues_serialized
+ relates_to_issues_related_serialized,
"blocking": queryset.filter(pk__in=blocking_issues)
.annotate(
relation_type=Value("blocking", output_field=CharField())
)
.values(*fields),
"blocked_by": queryset.filter(pk__in=blocked_by_issues)
.annotate(
relation_type=Value("blocked_by", output_field=CharField())
)
.values(*fields),
"duplicate": queryset.filter(pk__in=duplicate_issues)
.annotate(
relation_type=Value(
"duplicate",
output_field=CharField(),
)
)
.values(*fields)
| queryset.filter(pk__in=duplicate_issues_related)
.annotate(
relation_type=Value(
"duplicate",
output_field=CharField(),
)
)
.values(*fields),
"relates_to": queryset.filter(pk__in=relates_to_issues)
.annotate(
relation_type=Value(
"relates_to",
output_field=CharField(),
)
)
.values(*fields)
| queryset.filter(pk__in=relates_to_issues_related)
.annotate(
relation_type=Value(
"relates_to",
output_field=CharField(),
)
)
.values(*fields),
}
return Response(response_data, status=status.HTTP_200_OK)
@@ -99,6 +99,7 @@ class SubIssuesEndpoint(BaseAPIView):
),
)
.annotate(state_group=F("state__group"))
.order_by("-created_at")
)
# create's a dict with state group name with their respective issue id's
+19 -18
View File
@@ -17,6 +17,7 @@ from django.db.models import (
UUIDField,
Value,
Sum,
FloatField,
)
from django.db.models.functions import Coalesce, Cast
from django.core.serializers.json import DjangoJSONEncoder
@@ -138,7 +139,7 @@ class ModuleViewSet(BaseViewSet):
.values("issue_module__module_id")
.annotate(
completed_estimate_points=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("completed_estimate_points")[:1]
@@ -152,7 +153,7 @@ class ModuleViewSet(BaseViewSet):
.values("issue_module__module_id")
.annotate(
total_estimate_points=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("total_estimate_points")[:1]
@@ -166,7 +167,7 @@ class ModuleViewSet(BaseViewSet):
.values("issue_module__module_id")
.annotate(
backlog_estimate_point=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("backlog_estimate_point")[:1]
@@ -180,7 +181,7 @@ class ModuleViewSet(BaseViewSet):
.values("issue_module__module_id")
.annotate(
unstarted_estimate_point=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("unstarted_estimate_point")[:1]
@@ -194,7 +195,7 @@ class ModuleViewSet(BaseViewSet):
.values("issue_module__module_id")
.annotate(
started_estimate_point=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("started_estimate_point")[:1]
@@ -208,7 +209,7 @@ class ModuleViewSet(BaseViewSet):
.values("issue_module__module_id")
.annotate(
cancelled_estimate_point=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
)
)
.values("cancelled_estimate_point")[:1]
@@ -270,37 +271,37 @@ class ModuleViewSet(BaseViewSet):
.annotate(
backlog_estimate_points=Coalesce(
Subquery(backlog_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
unstarted_estimate_points=Coalesce(
Subquery(unstarted_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
started_estimate_points=Coalesce(
Subquery(started_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
cancelled_estimate_points=Coalesce(
Subquery(cancelled_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
completed_estimate_points=Coalesce(
Subquery(completed_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
total_estimate_points=Coalesce(
Subquery(total_estimate_point),
Value(0, output_field=IntegerField()),
Value(0, output_field=FloatField()),
),
)
.annotate(
@@ -475,12 +476,12 @@ class ModuleViewSet(BaseViewSet):
)
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
),
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -490,7 +491,7 @@ class ModuleViewSet(BaseViewSet):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
@@ -513,12 +514,12 @@ class ModuleViewSet(BaseViewSet):
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", IntegerField())
Cast("estimate_point__value", FloatField())
),
)
.annotate(
completed_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
@@ -528,7 +529,7 @@ class ModuleViewSet(BaseViewSet):
)
.annotate(
pending_estimates=Sum(
Cast("estimate_point__value", IntegerField()),
Cast("estimate_point__value", FloatField()),
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
+35 -30
View File
@@ -45,12 +45,21 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
archived = request.GET.get("archived", "false")
read = request.GET.get("read", None)
type = request.GET.get("type", "all")
mentioned = request.GET.get("mentioned", False)
q_filters = Q()
inbox_issue = Issue.objects.filter(
pk=OuterRef("entity_identifier"),
issue_inbox__status__in=[0, 2, -2],
workspace__slug=self.kwargs.get("slug"),
)
notifications = (
Notification.objects.filter(
workspace__slug=slug, receiver_id=request.user.id
)
.filter(entity_name="issue")
.annotate(is_inbox_issue=Exists(inbox_issue))
.select_related("workspace", "project", "triggered_by", "receiver")
.order_by("snoozed_till", "-created_at")
)
@@ -78,6 +87,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
if read == "true":
notifications = notifications.filter(read_at__isnull=False)
if mentioned:
notifications = notifications.filter(sender__icontains="mentioned")
else:
notifications = notifications.exclude(
sender__icontains="mentioned"
)
type = type.split(",")
# Subscribed issues
if "subscribed" in type:
@@ -202,46 +218,35 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug):
# Watching Issues Count
subscribed_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
snoozed_till__isnull=True,
entity_identifier__in=IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True),
).count()
unread_notifications_count = (
Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
snoozed_till__isnull=True,
)
.exclude(sender__icontains="mentioned")
.count()
)
# My Issues Count
my_issues_count = Notification.objects.filter(
mention_notifications_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
snoozed_till__isnull=True,
entity_identifier__in=IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True),
).count()
# Created Issues Count
created_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
snoozed_till__isnull=True,
entity_identifier__in=Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True),
sender__icontains="mentioned",
).count()
return Response(
{
"subscribed_issues": subscribed_issues_count,
"my_issues": my_issues_count,
"created_issues": created_issues_count,
"total_unread_notifications_count": int(
unread_notifications_count
),
"mention_unread_notifications_count": int(
mention_notifications_count
),
},
status=status.HTTP_200_OK,
)
+31 -2
View File
@@ -38,6 +38,7 @@ from plane.db.models import (
from ..base import BaseAPIView, BaseViewSet
from plane.bgtasks.page_transaction_task import page_transaction
from plane.bgtasks.page_version_task import page_version
def unarchive_archive_page_and_descendants(page_id, archived_at):
@@ -215,8 +216,14 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_404_NOT_FOUND,
)
else:
issue_ids = PageLog.objects.filter(
page_id=pk, entity_name="issue"
).values_list("entity_identifier", flat=True)
data = PageDetailSerializer(page).data
data["issue_ids"] = issue_ids
return Response(
PageDetailSerializer(page).data, status=status.HTTP_200_OK
data,
status=status.HTTP_200_OK,
)
def lock(self, request, slug, project_id, pk):
@@ -475,16 +482,38 @@ class PagesDescriptionViewSet(BaseViewSet):
status=472,
)
# Serialize the existing instance
existing_instance = json.dumps(
{
"description_html": page.description_html,
},
cls=DjangoJSONEncoder,
)
# Get the base64 data from the request
base64_data = request.data.get("description_binary")
# If base64 data is provided
if base64_data:
# Decode the base64 data to bytes
new_binary_data = base64.b64decode(base64_data)
# capture the page transaction
if request.data.get("description_html"):
page_transaction.delay(
new_value=request.data,
old_value=existing_instance,
page_id=pk,
)
# Store the updated binary data
page.description_binary = new_binary_data
page.description_html = request.data.get("description_html")
page.save()
# Return a success response
page_version.delay(
page_id=page.id,
existing_instance=existing_instance,
user_id=request.user.id,
)
return Response({"message": "Updated successfully"})
else:
return Response({"error": "No binary data provided"})
+37
View File
@@ -0,0 +1,37 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.db.models import PageVersion
from ..base import BaseAPIView
from plane.app.permissions import ProjectEntityPermission
from plane.app.serializers import PageVersionSerializer
class PageVersionEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, page_id, pk=None):
# Check if pk is provided
if pk:
# Return a single page version
page_version = PageVersion.objects.get(
workspace__slug=slug,
page_id=page_id,
pk=pk,
)
# Serialize the page version
serializer = PageVersionSerializer(page_version)
return Response(serializer.data, status=status.HTTP_200_OK)
# Return all page versions
page_versions = PageVersion.objects.filter(
workspace__slug=slug,
page_id=page_id,
)
# Serialize the page versions
serializer = PageVersionSerializer(page_versions, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -47,6 +47,7 @@ from plane.db.models import (
ProjectMember,
State,
Workspace,
IssueType,
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity
@@ -342,6 +343,13 @@ class ProjectViewSet(BaseViewSet):
.first()
)
# Create the issue type
IssueType.objects.create(
name="Task",
description="A task that needs to be done",
project_id=project.id,
)
model_activity.delay(
model_name="project",
model_id=str(project.id),
@@ -1,5 +1,4 @@
# Python imports
import re
# Django imports
from django.db.models import Q
@@ -11,13 +10,7 @@ from rest_framework.response import Response
# Module imports
from .base import BaseAPIView
from plane.db.models import (
Workspace,
Project,
Issue,
Cycle,
Module,
Page,
IssueView,
)
from plane.utils.issue_search import search_issues
+12
View File
@@ -37,6 +37,9 @@ from plane.utils.paginator import BasePaginator
from plane.authentication.utils.host import user_ip
from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
from plane.utils.host import base_host
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_cookie
class UserEndpoint(BaseViewSet):
@@ -47,6 +50,8 @@ class UserEndpoint(BaseViewSet):
return self.request.user
@cache_response(60 * 60)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def retrieve(self, request):
serialized_data = UserMeSerializer(request.user).data
return Response(
@@ -55,6 +60,8 @@ class UserEndpoint(BaseViewSet):
)
@cache_response(60 * 60)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def retrieve_user_settings(self, request):
serialized_data = UserMeSettingsSerializer(request.user).data
return Response(serialized_data, status=status.HTTP_200_OK)
@@ -79,6 +86,9 @@ class UserEndpoint(BaseViewSet):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/users/me/")
@invalidate_cache(
path="/api/users/me/workspaces/", multiple=True, user=False
)
def deactivate(self, request):
# Check all workspace user is active
user = self.get_object()
@@ -288,6 +298,8 @@ class AccountEndpoint(BaseAPIView):
class ProfileEndpoint(BaseAPIView):
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def get(self, request):
profile = Profile.objects.get(user=request.user)
serializer = ProfileSerializer(profile)
+11 -2
View File
@@ -44,6 +44,10 @@ from plane.db.models import (
WorkspaceTheme,
)
from plane.utils.cache import cache_response, invalidate_cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_cookie
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
class WorkSpaceViewSet(BaseViewSet):
@@ -118,7 +122,7 @@ class WorkSpaceViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
if serializer.is_valid():
if serializer.is_valid(raise_exception=True):
serializer.save(owner=request.user)
# Create Workspace member
_ = WorkspaceMember.objects.create(
@@ -171,6 +175,8 @@ class UserWorkSpacesEndpoint(BaseAPIView):
]
@cache_response(60 * 60 * 2)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def get(self, request):
fields = [
field
@@ -231,7 +237,10 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.filter(slug=slug).exists()
workspace = (
Workspace.objects.filter(slug=slug).exists()
or slug in RESTRICTED_WORKSPACE_SLUGS
)
return Response({"status": not workspace}, status=status.HTTP_200_OK)
+12 -10
View File
@@ -39,6 +39,16 @@ class OauthAdapter(Adapter):
self.client_secret = client_secret
self.code = code
def authentication_error_code(self):
if self.provider == "google":
return "GOOGLE_OAUTH_PROVIDER_ERROR"
elif self.provider == "github":
return "GITHUB_OAUTH_PROVIDER_ERROR"
elif self.provider == "gitlab":
return "GITLAB_OAUTH_PROVIDER_ERROR"
else:
return "OAUTH_NOT_CONFIGURED"
def get_auth_url(self):
return self.auth_url
@@ -62,7 +72,7 @@ class OauthAdapter(Adapter):
response.raise_for_status()
return response.json()
except requests.RequestException:
code = self._provider_error_code()
code = self.authentication_error_code()
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[code],
error_message=str(code),
@@ -77,15 +87,7 @@ class OauthAdapter(Adapter):
response.raise_for_status()
return response.json()
except requests.RequestException:
if self.provider == "google":
code = "GOOGLE_OAUTH_PROVIDER_ERROR"
elif self.provider == "github":
code = "GITHUB_OAUTH_PROVIDER_ERROR"
elif self.provider == "gitlab":
code = "GITLAB_OAUTH_PROVIDER_ERROR"
else:
code = "OAUTH_NOT_CONFIGURED"
code = self.authentication_error_code()
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[code],
error_message=str(code),
@@ -21,6 +21,7 @@ from plane.db.models import (
Cycle,
Module,
Issue,
IssueType,
IssueSequence,
IssueAssignee,
IssueLabel,
@@ -336,6 +337,12 @@ def create_issues(workspace, project, user_id, issue_count):
65535 if largest_sort_order is None else largest_sort_order + 10000
)
issue_type = IssueType.objects.create(
name="Task",
description="A task that needs to be completed.",
project=project,
)
for _ in range(0, issue_count):
start_date = [None, fake.date_this_year()][random.randint(0, 1)]
end_date = (
@@ -364,6 +371,7 @@ def create_issues(workspace, project, user_id, issue_count):
random.randint(0, 4)
],
created_by_id=creators[random.randint(0, len(creators) - 1)],
type=issue_type,
)
)
@@ -0,0 +1,44 @@
# Python imports
import json
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import Page, PageVersion
@shared_task
def page_version(
page_id,
existing_instance,
user_id,
):
# Get the page
page = Page.objects.get(id=page_id)
# Get the current instance
current_instance = (
json.loads(existing_instance) if existing_instance is not None else {}
)
# Create a version if description_html is updated
if current_instance.get("description_html") != page.description_html:
# Create a new page version
PageVersion.objects.create(
page_id=page_id,
workspace_id=page.workspace_id,
description_html=page.description_html,
description_binary=page.description_binary,
ownned_by_id=user_id,
last_saved_at=page.updated_at,
)
# If page versions are greater than 20 delete the oldest one
if PageVersion.objects.filter(page_id=page_id).count() > 20:
# Delete the old page version
PageVersion.objects.filter(page_id=page_id).order_by(
"last_saved_at"
).first().delete()
return
@@ -0,0 +1,275 @@
# Generated by Django 4.2.11 on 2024-07-01 06:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
def create_issue_types(apps, schema_editor):
Project = apps.get_model("db", "Project")
Issue = apps.get_model("db", "Issue")
IssueType = apps.get_model("db", "IssueType")
# Create the issue types for all projects
IssueType.objects.bulk_create(
[
IssueType(
name="Task",
description="A task that needs to be completed.",
project_id=project["id"],
workspace_id=project["workspace_id"],
)
for project in Project.objects.values("id", "workspace_id")
],
batch_size=1000,
)
# Update the issue type for all existing issues
issue_types = {
str(issue_type["project_id"]): str(issue_type["id"])
for issue_type in IssueType.objects.values("id", "project_id")
}
# Update the issue type for all existing issues
bulk_issues = []
for issue in Issue.objects.all():
issue.type_id = issue_types[str(issue.project_id)]
bulk_issues.append(issue)
# Update the issue type for all existing issues
Issue.objects.bulk_update(bulk_issues, ["type_id"], batch_size=1000)
def create_page_versions(apps, schema_editor):
Page = apps.get_model("db", "Page")
PageVersion = apps.get_model("db", "PageVersion")
# Create the page versions for all pages
PageVersion.objects.bulk_create(
[
PageVersion(
page_id=page["id"],
workspace_id=page["workspace_id"],
description_html=page["description_html"],
description_binary=page["description_binary"],
description_stripped=page["description_stripped"],
ownned_by_id=page["owned_by_id"],
last_saved_at=page["updated_at"],
)
for page in Page.objects.values(
"id",
"workspace_id",
"description_html",
"description_binary",
"description_stripped",
"owned_by_id",
"updated_at",
)
],
batch_size=1000,
)
class Migration(migrations.Migration):
dependencies = [
("db", "0069_alter_account_provider_and_more"),
]
operations = [
migrations.CreateModel(
name="IssueType",
fields=[
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("name", models.CharField(max_length=255)),
("description", models.TextField(blank=True)),
("logo_props", models.JSONField(default=dict)),
("sort_order", models.FloatField(default=65535)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
(
"is_default",
models.BooleanField(default=True),
),
],
options={
"verbose_name": "Issue Type",
"verbose_name_plural": "Issue Types",
"db_table": "issue_types",
"ordering": ("sort_order",),
},
),
migrations.AddField(
model_name="issue",
name="type",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="issue_type",
to="db.issuetype",
),
),
migrations.AddField(
model_name="apitoken",
name="is_service",
field=models.BooleanField(default=False),
),
migrations.RunPython(create_issue_types),
migrations.CreateModel(
name="PageVersion",
fields=[
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
(
"last_saved_at",
models.DateTimeField(default=django.utils.timezone.now),
),
("description_binary", models.BinaryField(null=True)),
(
"description_html",
models.TextField(blank=True, default="<p></p>"),
),
(
"description_stripped",
models.TextField(blank=True, null=True),
),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"ownned_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="page_versions",
to=settings.AUTH_USER_MODEL,
),
),
(
"page",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="page_versions",
to="db.page",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="page_versions",
to="db.workspace",
),
),
],
options={
"verbose_name": "Page Version",
"verbose_name_plural": "Page Versions",
"db_table": "page_versions",
"ordering": ("-created_at",),
},
),
migrations.AddField(
model_name="project",
name="start_date",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="project",
name="target_date",
field=models.DateTimeField(blank=True, null=True),
),
migrations.RunPython(create_page_versions),
]
+10 -1
View File
@@ -50,7 +50,14 @@ from .notification import (
Notification,
UserNotificationPreference,
)
from .page import Page, PageFavorite, PageLabel, PageLog, ProjectPage
from .page import (
Page,
PageFavorite,
PageLabel,
PageLog,
ProjectPage,
PageVersion,
)
from .project import (
Project,
ProjectBaseModel,
@@ -101,3 +108,5 @@ from .webhook import Webhook, WebhookLog
from .dashboard import Dashboard, DashboardWidget, Widget
from .favorite import UserFavorite
from .issue_type import IssueType
+1
View File
@@ -44,6 +44,7 @@ class APIToken(BaseModel):
null=True,
)
expired_at = models.DateTimeField(blank=True, null=True)
is_service = models.BooleanField(default=False)
class Meta:
verbose_name = "API Token"
+8
View File
@@ -91,6 +91,7 @@ class IssueManager(models.Manager):
| models.Q(issue_inbox__status=2)
| models.Q(issue_inbox__isnull=True)
)
.filter(state__is_triage=False)
.exclude(archived_at__isnull=False)
.exclude(project__archived_at__isnull=False)
.exclude(is_draft=True)
@@ -163,6 +164,13 @@ class Issue(ProjectBaseModel):
is_draft = models.BooleanField(default=False)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
type = models.ForeignKey(
"db.IssueType",
on_delete=models.SET_NULL,
related_name="issue_type",
null=True,
blank=True,
)
objects = models.Manager()
issue_objects = IssueManager()
+34
View File
@@ -0,0 +1,34 @@
# Django imports
from django.db import models
# Module imports
from .workspace import WorkspaceBaseModel
class IssueType(WorkspaceBaseModel):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
logo_props = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
is_default = models.BooleanField(default=True)
class Meta:
verbose_name = "Issue Type"
verbose_name_plural = "Issue Types"
db_table = "issue_types"
ordering = ("sort_order",)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
# If we are adding a new issue type, we need to set the sort order
if self._state.adding:
# Get the largest sort order for the project
largest_sort_order = IssueType.objects.filter(
project=self.project
).aggregate(largest=models.Max("sort_order"))["largest"]
# If there are issue types, set the sort order to the largest + 10000
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
super(IssueType, self).save(*args, **kwargs)
+50 -1
View File
@@ -1,6 +1,7 @@
import uuid
from django.conf import settings
from django.utils import timezone
# Django imports
from django.db import models
@@ -66,6 +67,15 @@ class Page(BaseModel):
"""Return owner email and page name"""
return f"{self.owned_by.email} <{self.name}>"
def save(self, *args, **kwargs):
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
super(Page, self).save(*args, **kwargs)
class PageLog(BaseModel):
TYPE_CHOICES = (
@@ -93,7 +103,9 @@ class PageLog(BaseModel):
verbose_name="Transaction Type",
)
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
"db.Workspace",
on_delete=models.CASCADE,
related_name="workspace_page_log",
)
class Meta:
@@ -247,3 +259,40 @@ class TeamPage(BaseModel):
verbose_name_plural = "Team Pages"
db_table = "team_pages"
ordering = ("-created_at",)
class PageVersion(BaseModel):
workspace = models.ForeignKey(
"db.Workspace",
on_delete=models.CASCADE,
related_name="page_versions",
)
page = models.ForeignKey(
"db.Page",
on_delete=models.CASCADE,
related_name="page_versions",
)
last_saved_at = models.DateTimeField(default=timezone.now)
ownned_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="page_versions",
)
description_binary = models.BinaryField(null=True)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
class Meta:
verbose_name = "Page Version"
verbose_name_plural = "Page Versions"
db_table = "page_versions"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
super(PageVersion, self).save(*args, **kwargs)
+3
View File
@@ -115,6 +115,9 @@ class Project(BaseModel):
related_name="default_state",
)
archived_at = models.DateTimeField(null=True)
# Project start and target date
start_date = models.DateTimeField(null=True, blank=True)
target_date = models.DateTimeField(null=True, blank=True)
def __str__(self):
"""Return name of the project"""
+2 -13
View File
@@ -5,6 +5,7 @@ from django.db import models
# Module imports
from .base import BaseModel
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
ROLE_CHOICES = (
(20, "Owner"),
@@ -112,19 +113,7 @@ def get_issue_props():
def slug_validator(value):
if value in [
"404",
"accounts",
"api",
"create-workspace",
"god-mode",
"installations",
"invitations",
"onboarding",
"profile",
"spaces",
"workspace-invitations",
]:
if value in RESTRICTED_WORKSPACE_SLUGS:
raise ValidationError("Slug is not valid")
@@ -23,6 +23,8 @@ from plane.license.utils.instance_value import (
get_configuration_value,
)
from plane.utils.cache import cache_response, invalidate_cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
class InstanceEndpoint(BaseAPIView):
@@ -36,6 +38,7 @@ class InstanceEndpoint(BaseAPIView):
]
@cache_response(60 * 60 * 2, user=False)
@method_decorator(cache_control(private=True, max_age=12))
def get(self, request):
instance = Instance.objects.first()
+3
View File
@@ -16,6 +16,7 @@ from django.core.management.utils import get_random_secret_key
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from corsheaders.defaults import default_headers
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -127,6 +128,8 @@ else:
CORS_ALLOW_ALL_ORIGINS = True
secure_origins = False
CORS_ALLOW_HEADERS = [*default_headers, "X-API-Key"]
# Application Settings
WSGI_APPLICATION = "plane.wsgi.application"
ASGI_APPLICATION = "plane.asgi.application"
+20 -8
View File
@@ -12,7 +12,7 @@ from django.db.models import (
Sum,
Value,
When,
IntegerField,
FloatField,
)
from django.db.models.functions import (
Coalesce,
@@ -98,7 +98,7 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
# Estimate
else:
queryset = queryset.annotate(
estimate=Sum(Cast("estimate_point__value", IntegerField()))
estimate=Sum(Cast("estimate_point__value", FloatField()))
).order_by(x_axis)
queryset = (
queryset.annotate(segment=F(segment)) if segment else queryset
@@ -137,7 +137,7 @@ def burndown_plot(
estimate__isnull=False,
estimate__type="points",
).exists()
if estimate_type and plot_type == "points":
if estimate_type and plot_type == "points" and cycle_id:
issue_estimates = Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
@@ -145,7 +145,18 @@ def burndown_plot(
estimate_point__isnull=False,
).values_list("estimate_point__value", flat=True)
issue_estimates = [int(value) for value in issue_estimates]
issue_estimates = [float(value) for value in issue_estimates]
total_estimate_points = sum(issue_estimates)
if estimate_type and plot_type == "points" and module_id:
issue_estimates = Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_module__module_id=module_id,
estimate_point__isnull=False,
).values_list("estimate_point__value", flat=True)
issue_estimates = [float(value) for value in issue_estimates]
total_estimate_points = sum(issue_estimates)
if cycle_id:
@@ -227,12 +238,12 @@ def burndown_plot(
.order_by("date")
)
for date in date_range:
if plot_type == "points":
if plot_type == "points":
for date in date_range:
cumulative_pending_issues = total_estimate_points
total_completed = 0
total_completed = sum(
int(item["estimate_point__value"])
float(item["estimate_point__value"])
for item in completed_issues_estimate_point_distribution
if item["date"] is not None and item["date"] <= date
)
@@ -241,7 +252,8 @@ def burndown_plot(
chart_data[str(date)] = None
else:
chart_data[str(date)] = cumulative_pending_issues
else:
else:
for date in date_range:
cumulative_pending_issues = total_issues
total_completed = 0
total_completed = sum(
+30
View File
@@ -0,0 +1,30 @@
RESTRICTED_WORKSPACE_SLUGS = [
"404",
"accounts",
"api",
"create-workspace",
"god-mode",
"installations",
"invitations",
"onboarding",
"profile",
"spaces",
"workspace-invitations",
"password",
"flags",
"monitor",
"monitoring",
"ingest",
"plane-pro",
"plane-ultimate",
"enterprise",
"plane-enterprise",
"disco",
"silo",
"chat",
"calendar",
"drive",
"channels",
"upgrade",
"billing",
]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.21.0",
"version": "0.22.0",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.21.0",
"version": "0.22.0",
"private": true,
"main": "./index.ts"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.21.0",
"version": "0.22.0",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
@@ -51,21 +51,6 @@ export const LinkEditView = ({
return text;
};
const isValidUrl = (urlString: string) => {
const urlPattern = new RegExp(
"^(https?:\\/\\/)?" + // validate protocol
"([\\w-]+\\.)+[\\w-]{2,}" + // validate domain name
"|((\\d{1,3}\\.){3}\\d{1,3})" + // validate IP (v4) address
"(\\:\\d+)?(\\/[-\\w.%]+)*" + // validate port and path
"(\\?[;&\\w.%=-]*)?" + // validate query string
"(\\#[-\\w]*)?$", // validate fragment locator
"i"
);
const regexTest = urlPattern.test(urlString);
const urlTest = isValidHttpUrl(urlString); // Ensure you have defined isValidHttpUrl
return regexTest && urlTest;
};
const handleUpdateLink = (url: string) => {
setLocalUrl(url);
};
@@ -74,7 +59,7 @@ export const LinkEditView = ({
() => () => {
if (linkRemoved.current) return;
const url = isValidUrl(localUrl) ? localUrl : viewProps.url;
const url = isValidHttpUrl(localUrl) ? localUrl : viewProps.url;
if (to >= editor.state.doc.content.size) return;
+1
View File
@@ -3,3 +3,4 @@ export * from "./embed";
export * from "./image";
export * from "./mention-suggestion";
export * from "./slash-commands-suggestion";
export * from "@/plane-editor/types";
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "eslint-config-custom",
"private": true,
"version": "0.21.0",
"version": "0.22.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
"version": "0.21.0",
"version": "0.22.0",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tsconfig",
"version": "0.21.0",
"version": "0.22.0",
"private": true,
"files": [
"base.json",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.21.0",
"version": "0.22.0",
"private": true,
"main": "./src/index.d.ts"
}
+2
View File
@@ -46,3 +46,5 @@ export type TPageFilters = {
sortBy: TPageFiltersSortBy;
filters?: TPageFilterProps;
};
export type TPageEmbedType = "mention" | "issue";
+12 -3
View File
@@ -50,6 +50,7 @@ export type TNotification = {
read_at: string | undefined;
archived_at: string | undefined;
snoozed_till: string | undefined;
is_inbox_issue: boolean | undefined;
workspace: string | undefined;
project: string | undefined;
created_at: string | undefined;
@@ -63,6 +64,7 @@ export type TNotificationPaginatedInfoQueryParams = {
type?: string | undefined;
snoozed?: boolean;
archived?: boolean;
mentioned?: boolean;
read?: boolean;
per_page?: number;
cursor?: string;
@@ -84,7 +86,14 @@ export type TNotificationPaginatedInfo = {
// notification count
export type TUnreadNotificationsCount = {
created_issues: number | undefined;
my_issues: number | undefined;
subscribed_issues: number | undefined;
total_unread_notifications_count: number;
mention_unread_notifications_count: number;
};
export type TNotificationLite = {
workspace_slug: string | undefined;
project_id: string | undefined;
notification_id: string | undefined;
issue_id: string | undefined;
is_inbox_issue: boolean | undefined;
};
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.21.0",
"version": "0.22.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Avatar } from "./avatar";
const meta: Meta<typeof Avatar> = {
@@ -0,0 +1,33 @@
import React, { FC } from "react";
import { DropdownIcon } from "../icons";
import { cn } from "../../helpers";
type Props = {
isOpen: boolean;
title: string;
hideChevron?: boolean;
indicatorElement?: React.ReactNode;
actionItemElement?: React.ReactNode;
};
export const CollapsibleButton: FC<Props> = (props) => {
const { isOpen, title, hideChevron = false, indicatorElement, actionItemElement } = props;
return (
<div className="flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-100">
<div className="flex items-center gap-3.5">
<div className="flex items-center gap-3">
{!hideChevron && (
<DropdownIcon
className={cn("size-2 text-custom-text-300 hover:text-custom-text-200 duration-300", {
"-rotate-90": !isOpen,
})}
/>
)}
<span className="text-base text-custom-text-100 font-medium">{title}</span>
</div>
{indicatorElement && indicatorElement}
</div>
{actionItemElement && isOpen && actionItemElement}
</div>
);
};
@@ -0,0 +1,52 @@
import React, { FC, useState, useEffect, useCallback } from "react";
import { Disclosure, Transition } from "@headlessui/react";
export type TCollapsibleProps = {
title: string | React.ReactNode;
children: React.ReactNode;
buttonClassName?: string;
isOpen?: boolean;
onToggle?: () => void;
defaultOpen?: boolean;
};
export const Collapsible: FC<TCollapsibleProps> = (props) => {
const { title, children, buttonClassName, isOpen, onToggle, defaultOpen } = props;
// state
const [localIsOpen, setLocalIsOpen] = useState<boolean>(isOpen || defaultOpen ? true : false);
useEffect(() => {
if (isOpen !== undefined) {
setLocalIsOpen(isOpen);
}
}, [isOpen]);
// handlers
const handleOnClick = useCallback(() => {
if (isOpen !== undefined) {
if (onToggle) onToggle();
} else {
setLocalIsOpen((prev) => !prev);
}
}, [isOpen, onToggle]);
return (
<Disclosure>
<Disclosure.Button className={buttonClassName} onClick={handleOnClick}>
{title}
</Disclosure.Button>
<Transition
show={localIsOpen}
className="overflow-hidden"
enter="transition-max-height duration-400 ease-in-out"
enterFrom="max-h-0"
enterTo="max-h-screen"
leave="transition-max-height duration-400 ease-in-out"
leaveFrom="max-h-screen"
leaveTo="max-h-0"
>
<Disclosure.Panel static>{children}</Disclosure.Panel>
</Transition>
</Disclosure>
);
};
+2
View File
@@ -0,0 +1,2 @@
export * from "./collapsible";
export * from "./collapsible-button";
+19 -4
View File
@@ -34,6 +34,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
onMenuClose,
tabIndex,
closeOnSelect,
openOnHover = false,
} = props;
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
@@ -68,12 +69,24 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
if (closeOnSelect) closeDropdown();
};
const handleMenuButtonClick = (e:React.MouseEvent<HTMLButtonElement, MouseEvent>)=>{
const handleMenuButtonClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault()
e.preventDefault();
isOpen ? closeDropdown() : openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}
};
const handleMouseEnter = () => {
if (openOnHover) openDropdown();
};
const handleMouseLeave = () => {
if (openOnHover && isOpen) {
setTimeout(() => {
closeDropdown();
}, 500);
}
};
useOutsideClickDetector(dropdownRef, closeDropdown);
@@ -111,6 +124,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
className={cn("relative w-min text-left", className)}
onKeyDownCapture={handleKeyDown}
onClick={handleOnClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{({ open }) => (
<>
@@ -203,4 +218,4 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
CustomMenu.MenuItem = MenuItem;
export { CustomMenu };
export { CustomMenu };
+1
View File
@@ -28,6 +28,7 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
onMenuClose?: () => void;
closeOnSelect?: boolean;
portalElement?: Element | null;
openOnHover?: boolean;
}
export interface ICustomSelectProps extends IDropdownProps {
+15
View File
@@ -0,0 +1,15 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const DropdownIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg
viewBox="0 0 7 5"
className={`${className} stroke-2`}
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<path d="M2.77267 4.0211L0.457719 1.78653C0.162864 1.50191 0.0961218 1.17467 0.257492 0.8048C0.418861 0.434934 0.70939 0.25 1.12908 0.25H5.72716C6.14685 0.25 6.43738 0.434934 6.59875 0.8048C6.76012 1.17467 6.69338 1.50191 6.39855 1.78653L4.08357 4.0211C3.98662 4.1147 3.88435 4.18329 3.77676 4.22687C3.66918 4.27046 3.55297 4.29225 3.42813 4.29225C3.30328 4.29225 3.18706 4.27046 3.07948 4.22687C2.97191 4.18329 2.86964 4.1147 2.77267 4.0211Z" />
</svg>
);
+1
View File
@@ -21,3 +21,4 @@ export * from "./related-icon";
export * from "./side-panel-icon";
export * from "./transfer-icon";
export * from "./info-icon";
export * from "./dropdown-icon";
+2
View File
@@ -20,3 +20,5 @@ export * from "./drag-handle";
export * from "./drop-indicator";
export * from "./favorite-star";
export * from "./loader";
export * from "./collapsible";
export * from "./popovers";
+1 -1
View File
@@ -17,7 +17,7 @@ export const ModalCore: React.FC<Props> = (props) => {
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose && handleClose}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose && handleClose()}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
+2
View File
@@ -0,0 +1,2 @@
export * from "./popover";
export * from "./popover-menu";
@@ -0,0 +1,44 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { PopoverMenu } from "./popover-menu";
const meta: Meta<typeof PopoverMenu> = {
title: "PopoverMenu",
component: PopoverMenu,
};
export default meta;
// types
type TPopoverMenu = {
id: number;
name: string;
};
type Story = StoryObj<typeof PopoverMenu<TPopoverMenu>>;
// data
const data: TPopoverMenu[] = [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Doe" },
{ id: 3, name: "John Smith" },
{ id: 4, name: "Jane Smith" },
];
// components
const PopoverMenuItemRender = (item: TPopoverMenu) => (
<div className="text-sm text-gray-600 hover:text-gray-700 rounded-sm cursor-pointer hover:bg-gray-200 transition-all px-1.5 py-0.5 capitalize">
{item.name}
</div>
);
// stories
export const Default: Story = {
args: {
popperPosition: "bottom-start",
panelClassName: "rounded bg-gray-100 p-2",
data: data,
keyExtractor: (item, index: number) => `${item.id}-${index}`,
render: (item) => PopoverMenuItemRender(item),
},
};
+39
View File
@@ -0,0 +1,39 @@
import React, { Fragment } from "react";
// components
import { Popover } from "./popover";
// helpers
import { cn } from "../../helpers";
// types
import { TPopoverMenu } from "./types";
export const PopoverMenu = <T,>(props: TPopoverMenu<T>) => {
const {
popperPosition = "bottom-end",
popperPadding = 0,
buttonClassName = "",
button,
panelClassName = "",
data,
keyExtractor,
render,
} = props;
return (
<Popover
popperPosition={popperPosition}
popperPadding={popperPadding}
buttonClassName={buttonClassName}
button={button}
panelClassName={cn(
"my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2 text-xs shadow-custom-shadow-rg focus:outline-none",
panelClassName
)}
>
<Fragment>
{data.map((item, index) => (
<Fragment key={keyExtractor(item, index)}>{render(item, index)}</Fragment>
))}
</Fragment>
</Popover>
);
};
@@ -0,0 +1,54 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Popover } from "./popover";
const meta: Meta<typeof Popover> = {
title: "Popover",
component: Popover,
};
export default meta;
// types
type Story = StoryObj<typeof Popover>;
// data
// components
const RenderCustomPopoverComponent = (
<div className="space-y-2">
<div className="text-sm font-medium text-gray-500">Your custom component</div>
<div>
{["option1", "option2", "option3"].map((option) => (
<div
key={option}
className="text-sm text-gray-600 hover:text-gray-700 rounded-sm cursor-pointer hover:bg-gray-200 transition-all px-1.5 py-0.5 capitalize"
>
{option}
</div>
))}
</div>
</div>
);
// stories
export const Default: Story = {
args: {
popperPosition: "bottom-start",
panelClassName: "rounded bg-gray-100 p-2",
children: RenderCustomPopoverComponent,
},
};
export const CustomMenuButton: Story = {
args: {
popperPosition: "bottom-start",
button: (
<div className="p-2 text-sm font-medium rounded bg-gray-100 hover:bg-gray-200 transition-all">
Custom Menu Button
</div>
),
panelClassName: "rounded bg-gray-100 p-2",
children: RenderCustomPopoverComponent,
},
};
+73
View File
@@ -0,0 +1,73 @@
import React, { Fragment, useState } from "react";
import { usePopper } from "react-popper";
import { Popover as HeadlessReactPopover, Transition } from "@headlessui/react";
// helpers
import { cn } from "../../helpers";
// types
import { TPopover } from "./types";
import { EllipsisVertical } from "lucide-react";
export const Popover = (props: TPopover) => {
const {
popperPosition = "bottom-end",
popperPadding = 0,
buttonClassName = "",
button,
panelClassName = "",
children,
} = props;
// states
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// react-popper derived values
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: popperPosition,
modifiers: [
{
name: "preventOverflow",
options: {
padding: popperPadding,
},
},
],
});
return (
<HeadlessReactPopover className="relative flex h-full w-full items-center justify-center">
<HeadlessReactPopover.Button ref={setReferenceElement} className="flex justify-center items-center">
{button ? (
button
) : (
<div
className={cn(
"flex justify-center items-center text-base h-6 w-6 rounded transition-all bg-custom-background-90 hover:bg-custom-background-80",
buttonClassName
)}
>
<EllipsisVertical className="h-3 w-3" />
</div>
)}
</HeadlessReactPopover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<HeadlessReactPopover.Panel
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className={cn("absolute left-0 top-full z-20 w-screen max-w-xs mt-2", panelClassName)}
>
{children}
</HeadlessReactPopover.Panel>
</Transition>
</HeadlessReactPopover>
);
};
+27
View File
@@ -0,0 +1,27 @@
import { ReactNode } from "react";
import { Placement } from "@popperjs/core";
export type TPopoverButtonDefaultOptions = {
// button and button styling
button?: ReactNode;
buttonClassName?: string;
};
export type TPopoverDefaultOptions = TPopoverButtonDefaultOptions & {
// popper styling
popperPosition?: Placement | undefined;
popperPadding?: number | undefined;
// panel styling
panelClassName?: string;
};
export type TPopover = TPopoverDefaultOptions & {
// children
children: ReactNode;
};
export type TPopoverMenu<T> = TPopoverDefaultOptions & {
data: T[];
keyExtractor: (item: T, index: number) => string;
render: (item: T, index: number) => ReactNode;
};
@@ -87,7 +87,7 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options className="absolute left-0 z-10 mt-1 min-w-[8rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
<Listbox.Options className="absolute left-0 z-10 mt-1 min-w-[12rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
<div className="space-y-1 p-2">
{PEEK_MODES.map((mode) => (
<Listbox.Option
@@ -95,24 +95,14 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
value={mode.key}
className={({ active, selected }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || selected ? "bg-custom-background-80" : ""
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5">
<Icon
iconName={mode.icon}
className={`-my-1 flex-shrink-0 !text-base ${mode.key === "full" ? "rotate-45" : ""}`}
/>
{mode.label}
</div>
</div>
{selected && <Icon iconName="done" />}
</div>
)}
<div className="flex items-center gap-1.5">
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
{mode.label}
</div>
</Listbox.Option>
))}
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.21.0",
"version": "0.22.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -3,9 +3,10 @@
import { observer } from "mobx-react";
// components
import { PageHead } from "@/components/core";
import { WorkspaceActiveCyclesUpgrade } from "@/components/workspace";
// hooks
import { useWorkspace } from "@/hooks/store";
// plane web components
import { WorkspaceActiveCyclesRoot } from "@/plane-web/components/active-cycles";
const WorkspaceActiveCyclesPage = observer(() => {
const { currentWorkspace } = useWorkspace();
@@ -15,7 +16,7 @@ const WorkspaceActiveCyclesPage = observer(() => {
return (
<>
<PageHead title={pageTitle} />
<WorkspaceActiveCyclesUpgrade />
<WorkspaceActiveCyclesRoot />
</>
);
});
@@ -6,9 +6,7 @@ import { NotificationsSidebar } from "@/components/workspace-notifications";
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
return (
<div className="relative w-full h-full overflow-hidden flex items-center">
<div className="relative w-full lg:w-2/6 border-0 lg:border-r border-custom-border-200 z-[10] flex-shrink-0 bg-custom-background-100 h-full transition-all overflow-hidden">
<NotificationsSidebar />
</div>
<NotificationsSidebar />
<div className="w-full h-full overflow-hidden overflow-y-auto">{children}</div>
</div>
);
@@ -3,19 +3,32 @@
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common";
import { PageHead } from "@/components/core";
import { InboxContentRoot } from "@/components/inbox";
import { IssuePeekOverview } from "@/components/issues";
// constants
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
// hooks
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
import { useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
const WorkspaceDashboardPage = observer(() => {
// hooks
const { currentWorkspace } = useWorkspace();
const { notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
const {
currentSelectedNotificationId,
setCurrentSelectedNotificationId,
notificationLiteByNotificationId,
notificationIdsByWorkspaceId,
getNotifications,
} = useWorkspaceNotifications();
const {
membership: { fetchUserProjectInfo },
} = useUser();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
notificationLiteByNotificationId(currentSelectedNotificationId);
// fetch workspace notifications
const notificationMutation =
@@ -29,15 +42,46 @@ const WorkspaceDashboardPage = observer(() => {
useSWR(
currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
currentWorkspace?.slug
? async () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
: null
);
// fetching user project member info
const { isLoading: projectMemberInfoLoader } = useSWR(
workspace_slug && project_id && is_inbox_issue
? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}`
: null,
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
);
return (
<>
<PageHead title={pageTitle} />
<div className="w-full h-full overflow-hidden overflow-y-auto">
<IssuePeekOverview embedIssue />
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
<>
{projectMemberInfoLoader ? (
<div className="w-full h-full flex justify-center items-center">
<LogoSpinner />
</div>
) : (
<InboxContentRoot
setIsMobileSidebar={() => {}}
isMobileSidebar={false}
workspaceSlug={workspace_slug}
projectId={project_id}
inboxIssueId={issue_id}
isNotificationEmbed
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</>
) : (
<IssuePeekOverview
embedIssue
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</div>
</>
);
@@ -20,7 +20,7 @@ import {
ISSUE_LAYOUTS,
} from "@/constants/issue";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel } from "@/hooks/store";
@@ -108,8 +108,6 @@ export const ProfileIssuesMobileHeader = observer(() => {
[workspaceSlug, updateFilters, userId]
);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<div className="flex justify-evenly border-b border-custom-border-200 py-2 md:hidden">
<CustomMenu
@@ -146,7 +144,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
isFiltersApplied={isFiltersApplied}
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
layoutDisplayFiltersOptions={
@@ -24,7 +24,7 @@ import {
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { cn } from "@/helpers/common.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
import { truncateText } from "@/helpers/string.helper";
// hooks
import {
@@ -152,7 +152,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
const issuesCount = getGroupIssueCount(undefined, undefined, false);
return (
@@ -248,7 +247,11 @@ export const CycleIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
<FiltersDropdown
title="Filters"
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@@ -12,9 +12,15 @@ import { CustomMenu } from "@plane/ui";
import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
ISSUE_LAYOUTS,
} from "@/constants/issue";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
@@ -109,8 +115,6 @@ export const CycleIssuesMobileHeader = () => {
[workspaceSlug, projectId, cycleId, updateFilters]
);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<>
<ProjectAnalyticsModal
@@ -150,7 +154,7 @@ export const CycleIssuesMobileHeader = () => {
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
</span>
}
isFiltersApplied={isFiltersApplied}
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
@@ -98,4 +98,4 @@ const ProjectCyclesPage = observer(() => {
);
});
export default ProjectCyclesPage;
export default ProjectCyclesPage;
@@ -18,7 +18,7 @@ import {
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "@/constants/issue";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -90,8 +90,6 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
: currentProjectDetails.draft_issues
: undefined;
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
@@ -140,7 +138,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@@ -22,8 +22,8 @@ import {
} from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { SPACE_BASE_URL } from "@/helpers/common.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import {
useEventTracker,
@@ -107,13 +107,12 @@ export const ProjectIssuesHeader = observer(() => {
},
[workspaceSlug, projectId, updateFilters]
);
const publishedURL = `${SPACE_BASE_URL}/issues/${currentProjectDetails?.anchor}`;
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
const issuesCount = getGroupIssueCount(undefined, undefined, false);
return (
@@ -193,7 +192,7 @@ export const ProjectIssuesHeader = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@@ -13,9 +13,15 @@ import { CustomMenu } from "@plane/ui";
import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts";
// constants
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
ISSUE_LAYOUTS,
} from "@/constants/issue";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
@@ -88,8 +94,6 @@ export const ProjectIssuesMobileHeader = observer(() => {
[workspaceSlug, projectId, updateFilters]
);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<>
<ProjectAnalyticsModal
@@ -129,7 +133,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
isFiltersApplied={isFiltersApplied}
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
@@ -24,7 +24,7 @@ import {
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { cn } from "@/helpers/common.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
import { truncateText } from "@/helpers/string.helper";
// hooks
import {
@@ -152,7 +152,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
const issuesCount = getGroupIssueCount(undefined, undefined, false);
return (
@@ -249,7 +248,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
<FiltersDropdown
title="Filters"
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@@ -13,9 +13,15 @@ import { CustomMenu } from "@plane/ui";
import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
ISSUE_LAYOUTS,
} from "@/constants/issue";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
@@ -90,8 +96,6 @@ export const ModuleIssuesMobileHeader = observer(() => {
[workspaceSlug, projectId, moduleId, updateFilters]
);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<div className="block md:hidden">
<ProjectAnalyticsModal
@@ -131,7 +135,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
isFiltersApplied={isFiltersApplied}
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
@@ -56,7 +56,7 @@ const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props) => {
<div className="w-80 flex-shrink-0 overflow-y-hidden pt-8 sm:hidden hidden md:block lg:block">
<ProjectSettingsSidebar />
</div>
<div className="w-full pl-10 sm:pl-10 md:pl-0 lg:pl-0 overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md">
<div className="w-full pl-10 sm:pl-10 md:pl-3 lg:pl-3 overflow-y-scroll vertical-scrollbar scrollbar-md">
{children}
</div>
</div>
@@ -1,25 +1,30 @@
"use client";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { PageHead } from "@/components/core";
import { ProjectSettingStateList } from "@/components/states";
import { ProjectStateRoot } from "@/components/project-states";
// hook
import { useProject } from "@/hooks/store";
const StatesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams();
// store
const { currentProjectDetails } = useProject();
// derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined;
return (
<>
<PageHead title={pageTitle} />
<div className="w-full gap-10 overflow-y-auto py-8 pr-9">
<div className="py-8 pr-9">
<div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">States</h3>
</div>
<ProjectSettingStateList />
{workspaceSlug && projectId && (
<ProjectStateRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
)}
</div>
</>
);
@@ -22,7 +22,7 @@ import {
import { EUserProjectRoles } from "@/constants/project";
import { EViewAccess } from "@/constants/views";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
import { truncateText } from "@/helpers/string.helper";
// hooks
import {
@@ -133,8 +133,6 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
@@ -233,7 +231,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
title="Filters"
placement="bottom-end"
disabled={!canUserCreateIssue}
isFiltersApplied={isFiltersApplied}
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
@@ -34,7 +34,7 @@ const GlobalViewIssuesPage = observer(() => {
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
{globalViewId && <GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} />}
<AllIssueLayoutRoot />
<AllIssueLayoutRoot isDefaultView={!!defaultView} />
</div>
</div>
</>
@@ -15,7 +15,7 @@ import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useLabel, useMember, useUser, useIssues, useGlobalView } from "@/hooks/store";
@@ -98,7 +98,6 @@ export const GlobalIssuesHeader = observer(() => {
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
const isLocked = viewDetails?.is_locked;
return (
@@ -118,7 +117,11 @@ export const GlobalIssuesHeader = observer(() => {
<div className="flex items-center gap-2">
{!isLocked && (
<>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
<FiltersDropdown
title="Filters"
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
filters={issueFilters?.filters ?? {}}
+13
View File
@@ -5,6 +5,8 @@ import "@/styles/globals.css";
import "@/styles/command-pallette.css";
import "@/styles/emoji.css";
import "@/styles/react-day-picker.css";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// local
import { AppProvider } from "./provider";
@@ -36,6 +38,17 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest.json" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
{/* preloading */}
<link rel="preload" href={`${API_BASE_URL}/api/instances/`} as="fetch" crossOrigin="use-credentials" />
<link rel="preload" href={`${API_BASE_URL}/api/users/me/ `} as="fetch" crossOrigin="use-credentials" />
<link rel="preload" href={`${API_BASE_URL}/api/users/me/profile/ `} as="fetch" crossOrigin="use-credentials" />
<link rel="preload" href={`${API_BASE_URL}/api/users/me/settings/ `} as="fetch" crossOrigin="use-credentials" />
<link
rel="preload"
href={`${API_BASE_URL}/api/users/me/workspaces/`}
as="fetch"
crossOrigin="use-credentials"
/>
</head>
<body>
<div id="context-menu-portal" />
+2
View File
@@ -0,0 +1,2 @@
export * from "./root";
export * from "./workspace-active-cycles-upgrade";

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