Compare commits

..
Author SHA1 Message Date
NarayanBavisetti 509618746c Merge branch 'preview' of github.com:makeplane/plane into fix-intake-cycle-module-issue 2024-10-22 18:57:33 +05:30
NarayanBavisetti c76b247abd chore: changed the annotate for cycle id 2024-10-22 18:32:58 +05:30
d859ab9c39 [WEB-2708] fix: intake module and cycle addition fixed (#5890)
* fix: intake module and cycle addition fixed

* chore: fixed the search endpoint

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-10-22 17:59:07 +05:30
NarayanBavisetti c462b00889 Merge branch 'preview' of github.com:makeplane/plane into fix-intake-cycle-module-issue 2024-10-22 17:19:11 +05:30
NarayanBavisetti d6c0e1945e chore: fixed the search endpoint 2024-10-22 16:42:27 +05:30
gakshita 9d096cd1ab fix: intake module and cycle addition fixed 2024-10-22 16:36:18 +05:30
Anmol Singh BhatiaandGitHub 36b868e375 [WEB-2707] fix: draft issue module update and code refactor (#5889)
* chore: draft issue module update

* chore: code refactor
2024-10-22 16:16:29 +05:30
Aaryan KhandelwalandGitHub 4c20be6cf2 [PE-68] fix: markdown transformation of mention and custom image components (#5864)
* fix: markdown content for mention and custom image extensions

* style: update issue embed upgrade card

* chore: added string escapes
2024-10-22 14:29:50 +05:30
Bavisetti NarayanandGitHub 7bf4620bc1 chore: soft deletion of cycle and module (#5884)
* chore: soft deletion of cycle and module

* chore: cycle module soft delete

* chore: added the deletion task

* chore: updated the env example

* chore: cycle issue unique constraints

* chore: udpated the Q operator
2024-10-22 14:21:26 +05:30
NikhilandGitHub 00eff43f4d fix: bucket policy script to handle error conditions (#5887)
* fix: bucket policy script to handle error conditions

* dev: handle edge cases
2024-10-22 14:19:43 +05:30
sriram veeraghanta 3d3f1b8f74 fix: typescript version consistency 2024-10-22 14:13:28 +05:30
sriram veeraghantaandGitHub b87516b0be chore: fixing inconsistent dependencies across the platform (#5885)
* chore: fixing inconsistent dependies across the platform

* fix: fixing peer dependencies

* chore: yarn lock regeneration
2024-10-22 14:03:34 +05:30
Anmol Singh BhatiaandGitHub 8a1d3c4cf9 chore: urgent priority icon improvement (#5879) 2024-10-22 13:25:22 +05:30
Akshita GoyalandGitHub 0f25f39404 WEB-2381 Chore: intake refactor (#5752)
* chore: intake emails and forms

* fix: moved files to ee

* fix: intake form ui

* fix: settings apis integrated

* fix: removed publish api

* fix: removed space app

* fix: lint issue

* fix: removed logs

* fix: removed comment

* fix: improved success image
2024-10-22 12:09:03 +05:30
sriram veeraghanta fb49644185 fix: renaming the action and formatting 2024-10-21 19:26:16 +05:30
NikhilandGitHub b745a29454 fix: credential sending for file uploads (#5869) 2024-10-21 17:46:46 +05:30
M. PalanikannanandGitHub c940a2921e fix: validation of public and private assets (#5878) 2024-10-21 15:59:44 +05:30
Anmol Singh BhatiaandGitHub 6f8df3279c [WEB-2681] fix: module progress indicator (#5842)
* fix: module progress indicator

* fix: module progress indicator
2024-10-21 15:48:35 +05:30
Prateek ShouryaandGitHub b833e3b10c [WEB-2674] chore: open parent issues in peek-overview from the parent badge. (#5872)
* [WEB-2674] chore: open parent issues in peek-overview from the parent badge.

* chore: remove `_blank` target from ControlLink.
2024-10-21 14:20:00 +05:30
M. PalanikannanandGitHub 5a0dc4a65a [PE-69] fix: image restoration fixed for new images in private bucket (#5839)
* regression: image aspect ratio fix

* fix: name of variables changed for clarity

* fix: restore only on error

* fix: restore image by handling it inside the image component

* fix: image restoration fixed and aspect ratio added to old images to stop updates on load

* fix: added back restoring logic for public images

* fix: add conditions

* fix: image attributes types

* fix: return for old images

* fix: remove passive false

* fix: eslint fixes

* fix: stopping infinite loading scenarios while restoring from error
2024-10-21 14:17:05 +05:30
Ketan SharmaandGitHub e866571e04 fix backend (#5875) 2024-10-21 13:07:36 +05:30
Bavisetti NarayanandGitHub 3c3fc7cd6d chore: draft issue listing (#5874) 2024-10-21 13:02:20 +05:30
Bavisetti NarayanandGitHub db919420a7 [WEB-2693] chore: removed the deleted cycles from the issue list (#5868)
* chore: added the deleted cycles from list

* chore: removed the extra annotation

* chore: removed the frontend comment
2024-10-18 15:48:34 +05:30
M. PalanikannanandGitHub 2982cd47a9 fix: remoteImageSrc to come from resolved source (#5867) 2024-10-18 14:21:07 +05:30
M. PalanikannanandGitHub 81550ab5ef [PE-56] regression: image aspect ratio fix (#5792)
* regression: image aspect ratio fix

* fix: name of variables changed for clarity
2024-10-18 13:40:39 +05:30
Bavisetti NarayanandGitHub 07402efd79 chore: filtered the deleted labels and modules (#5860) 2024-10-18 13:20:32 +05:30
Prateek ShouryaandGitHub 46302f41bc fix: improvements for project types. (#5857) 2024-10-18 11:08:07 +05:30
Ketan SharmaandGitHub 9530884c59 fix the logic (#5807) 2024-10-17 17:08:49 +05:30
Prateek ShouryaandGitHub 173b49b4cb [WEB-2431] chore: profile settings page UI improvement (#5838)
* [WEB-2431] chore: timezone and language management.

* chore: remove project level timezone changes.

* chore: minor UI improvement.

* chore: minor improvements
2024-10-17 17:06:22 +05:30
Anmol Singh BhatiaandGitHub e581ac890e chore: workspace collaborators improvements (#5846) 2024-10-17 17:05:21 +05:30
a7b58e4a93 [WEB-2625] chore: workspace favorite and draft improvement (#5855)
* chore: favorite empty state updated

* chore: added draft issue count in workspace members

* chore: workspace draft count improvement

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-10-17 17:02:25 +05:30
Bavisetti NarayanandGitHub d552913171 chore: updated queryset for soft delete (#5844) 2024-10-17 17:01:26 +05:30
Bavisetti NarayanandGitHub b6a7e45e8d chore: added draft cycle and module in draft issue (#5854) 2024-10-17 13:35:13 +05:30
Aaryan KhandelwalandGitHub 6209aeec0b fix: color extension not working on issue description and published page (#5852)
* fix: color extension not working

* chore: update types
2024-10-17 13:26:23 +05:30
Anmol Singh BhatiaandGitHub 1099c59b83 fix: draft issue empty state flicker (#5848) 2024-10-17 12:55:32 +05:30
NikhilandGitHub 9b2ffaaca8 fix: draft issue asset conversion to issue (#5849) 2024-10-17 12:51:13 +05:30
sriram veeraghanta aa93cca7bf fix: workflow fixes 2024-10-16 21:07:01 +05:30
sriram veeraghanta 1191f74bfe fix: workflow fixes 2024-10-16 20:08:25 +05:30
sriram veeraghanta fbd1f6334a fix: workflow fixes 2024-10-16 20:05:10 +05:30
Anmol Singh BhatiaandGitHub 7d36d63eb1 [WEB-2682] fix: delete project mutation and workspace draft header validation (#5843)
* fix: workspace draft header action validation

* fix: delete project mutation
2024-10-16 16:13:26 +05:30
NikhilandGitHub 9b85306359 dev: move storage metadata collection to background job (#5818)
* fix: move storage metadata collection to background job

* fix: docker compose and env

* fix: archive endpoint
2024-10-16 13:55:49 +05:30
cc613e57c9 chore: delete deprecated tables (#5833)
* migration: external source and id for issues

* fix: cleaning up deprecated favorite tables

* fix: removing deprecated models

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-10-16 00:33:57 +05:30
Bavisetti NarayanandGitHub 6e63af7ca9 [WEB-2626] chore: removed the deleted issue's count (#5837)
* chore: removed the deleted issue count

* chore: added issue manager in burn down
2024-10-16 00:30:44 +05:30
guru_sainathandGitHub 5f9af92faf fix: attachment_count in issue pagination v2 endpoint (#5840)
* fix: attachemnt_count in the issue pagination v2 endpoint

* fix: string comparision in description check in params
2024-10-15 23:46:57 +05:30
Anmol Singh BhatiaandGitHub 4e70e894f6 chore: workspace draft issue type (#5836) 2024-10-15 18:59:22 +05:30
Anmol Singh BhatiaandGitHub ff090ecf39 fix: workspace draft move to project (#5834) 2024-10-15 17:14:56 +05:30
Akshita GoyalandGitHub 645a261493 fix: Added a common dropdown component (#5826)
* fix: Added a common dropdown component

* fix: dropdown

* fix: estimate dropdown

* fix: removed consoles
2024-10-15 15:17:46 +05:30
Prateek ShouryaandGitHub 8d0611b2a7 [WEB-2613] chore: open parent and sibling issue in new tab from peek-overview/ issue detail page. (#5819) 2024-10-15 13:37:52 +05:30
Bavisetti NarayanandGitHub 3d7d3c8af1 [WEB-2631] chore: changed the cascading logic for soft delete (#5829)
* chore: changed the cascading logic for soft delete

* chore: changed the delete key

* chore: added the key on delete in project base model
2024-10-15 13:30:44 +05:30
Prateek ShouryaandGitHub 662b99da92 [WEB-2577] improvement: use common create/update issue modal for accepting intake issues for consistency (#5830)
* [WEB-2577] improvement: use common create/update issue modal for accepting intake issues for consistency

* fix: lint errors.

* chore: minor UX copy fix.

* chore: minor indentation fix.
2024-10-15 13:11:14 +05:30
Prateek ShouryaandGitHub fa25a816a7 [WEB-2549] chore: ux copy update for project access. (#5831) 2024-10-15 12:57:29 +05:30
Anmol Singh BhatiaandGitHub ee823d215e [WEB-2629] chore: workspace draft issue ux copy updated (#5825)
* chore: workspace draft issue ux copy updated

* chore: workspace draft issue ux copy updated
2024-10-14 17:26:54 +05:30
Akshita GoyalandGitHub 4b450f8173 fix: moved dropdowns to chart component + added pending icon (#5824)
* fix: moved dropdowns to chart component + added pending icon

* fix: copy changes

* fix: review changes
2024-10-14 17:00:58 +05:30
Anmol Singh BhatiaandGitHub 36229d92e0 [WEB-2629] fix: workspace draft delete and move mutation (#5822)
* fix: mutation fix

* chore: code refactor

* chore: code refactor

* chore: useWorkspaceIssueProperties added
2024-10-14 16:50:19 +05:30
Anmol Singh BhatiaandGitHub cb90810d02 chore: double click action added and code refactor (#5821) 2024-10-14 16:46:08 +05:30
Anmol Singh BhatiaandGitHub 658542cc62 [WEB-2616] fix: issue widget attachment (#5820)
* fix: issue widget attachment

* chore: comment added
2024-10-14 16:32:31 +05:30
166 changed files with 4168 additions and 3505 deletions
+10 -10
View File
@@ -170,7 +170,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Admin Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -196,7 +196,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Web Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -222,7 +222,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Space Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -248,7 +248,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Live Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -274,7 +274,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Backend Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -300,7 +300,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Proxy Build and Push
uses: ./.github/actions/buildpush-action
uses: ./.github/actions/build-push-ce
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
@@ -320,7 +320,7 @@ jobs:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
name: Attach Assets to Build
runs-on: ubuntu-20.04
needs: [ branch_build_setup ]
needs: [branch_build_setup]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -340,7 +340,7 @@ jobs:
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env
publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release
@@ -353,7 +353,7 @@ jobs:
branch_build_push_space,
branch_build_push_live,
branch_build_push_apiserver,
branch_build_push_proxy
branch_build_push_proxy,
]
env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
@@ -380,4 +380,4 @@ jobs:
${{ github.workspace }}/deploy/selfhost/setup.sh
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env
${{ github.workspace }}/deploy/selfhost/variables.env
@@ -8,14 +8,13 @@ on:
env:
CURRENT_BRANCH: ${{ github.ref_name }}
TARGET_BRANCH: ${{ vars.SYNC_TARGET_BRANCH_NAME }} # The target branch that you would like to merge changes like develop
TARGET_BRANCH: "preview" # The target branch that you would like to merge changes like develop
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
REVIEWER: ${{ vars.SYNC_PR_REVIEWER }}
ACCOUNT_USER_NAME: ${{ vars.ACCOUNT_USER_NAME }}
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
jobs:
Create_PR:
create_pull_request:
runs-on: ubuntu-latest
permissions:
pull-requests: write
@@ -48,6 +47,6 @@ jobs:
echo "Pull Request already exists: $PR_EXISTS"
else
echo "Creating new pull request"
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $CURRENT_BRANCH --title "sync: community changes" --body "")
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $CURRENT_BRANCH --title "${{ vars.SYNC_PR_TITLE }}" --body "")
echo "Pull Request created: $PR_URL"
fi
@@ -35,9 +35,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
RUN_ID="${{ github.run_id }}"
TARGET_REPO="${{ vars.SYNC_TARGET_REPO }}"
TARGET_BRANCH="sync/${RUN_ID}"
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
+1 -1
View File
@@ -41,7 +41,7 @@
"@plane/eslint-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.16.1",
"@types/react": "^18.2.48",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.8",
"@types/zxcvbn": "^4.4.4",
+2 -1
View File
@@ -57,5 +57,6 @@ ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=
# Hard delete files after days
HARD_DELETE_AFTER_DAYS=60
HARD_DELETE_AFTER_DAYS=60
-4
View File
@@ -405,10 +405,6 @@ class CycleAPIEndpoint(BaseAPIView):
)
# Delete the cycle
cycle.delete()
# Delete the cycle issues
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk"),
).delete()
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="cycle",
+9 -2
View File
@@ -227,7 +227,10 @@ class InboxIssueAPIEndpoint(BaseAPIView):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -235,7 +238,11 @@ class InboxIssueAPIEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
+9 -1
View File
@@ -16,6 +16,7 @@ from django.db.models import (
Q,
Value,
When,
Subquery,
)
from django.utils import timezone
@@ -48,6 +49,7 @@ from plane.db.models import (
Label,
Project,
ProjectMember,
CycleIssue,
)
from .base import BaseAPIView
@@ -202,7 +204,13 @@ class IssueAPIEndpoint(BaseAPIView):
issue_queryset = (
self.get_queryset()
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
+1 -1
View File
@@ -204,7 +204,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
if cycle_id != "not_provided":
DraftIssueCycle.objects.filter(draft_issue=instance).delete()
if cycle_id is not None:
if cycle_id:
DraftIssueCycle.objects.create(
cycle_id=cycle_id,
draft_issue=instance,
@@ -65,6 +65,7 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
class WorkspaceMemberMeSerializer(BaseSerializer):
draft_issue_count = serializers.IntegerField(read_only=True)
class Meta:
model = WorkspaceMember
fields = "__all__"
+4 -1
View File
@@ -110,7 +110,10 @@ class AnalyticsEndpoint(BaseAPIView):
if x_axis in ["labels__id"] or segment in ["labels__id"]:
label_details = (
Issue.objects.filter(
workspace__slug=slug, **filters, labels__id__isnull=False
workspace__slug=slug,
**filters,
labels__id__isnull=False
& Q(label_issue__deleted_at__isnull=True),
)
.distinct("labels__id")
.order_by("labels__id")
+7 -15
View File
@@ -22,6 +22,7 @@ from plane.db.models import (
from plane.settings.storage import S3Storage
from plane.app.permissions import allow_permission, ROLE
from plane.utils.cache import invalidate_cache_directly
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
class UserAssetsV2Endpoint(BaseAPIView):
@@ -193,14 +194,11 @@ class UserAssetsV2Endpoint(BaseAPIView):
def patch(self, request, asset_id):
# get the asset id
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
storage = S3Storage(request=request)
# get the storage metadata
asset.is_uploaded = True
# get the storage metadata
if asset.storage_metadata is None:
asset.storage_metadata = storage.get_object_metadata(
object_name=asset.asset.name
)
if not asset.storage_metadata:
get_asset_object_metadata.delay(asset_id=str(asset_id))
# get the entity and save the asset id for the request field
self.entity_asset_save(
asset_id=asset_id,
@@ -446,14 +444,11 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
def patch(self, request, slug, asset_id):
# get the asset id
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
storage = S3Storage(request=request)
# get the storage metadata
asset.is_uploaded = True
# get the storage metadata
if asset.storage_metadata is None:
asset.storage_metadata = storage.get_object_metadata(
object_name=asset.asset.name
)
if not asset.storage_metadata:
get_asset_object_metadata.delay(asset_id=str(asset_id))
# get the entity and save the asset id for the request field
self.entity_asset_save(
asset_id=asset_id,
@@ -686,14 +681,11 @@ class ProjectAssetEndpoint(BaseAPIView):
asset = FileAsset.objects.get(
id=pk,
)
storage = S3Storage(request=request)
# get the storage metadata
asset.is_uploaded = True
# get the storage metadata
if asset.storage_metadata is None:
asset.storage_metadata = storage.get_object_metadata(
object_name=asset.asset.name
)
if not asset.storage_metadata:
get_asset_object_metadata.delay(asset_id=str(pk))
# update the attributes
asset.attributes = request.data.get("attributes", asset.attributes)
+26 -1
View File
@@ -160,6 +160,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -171,6 +172,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -182,6 +184,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -193,6 +196,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -204,6 +208,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -215,6 +220,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -514,7 +520,26 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar_url=F("assignees__avatar_url"))
.annotate(
avatar_url=Case(
# If `avatar_asset` exists, use it to generate the asset URL
When(
assignees__avatar_asset__isnull=False,
then=Concat(
Value("/api/assets/v2/static/"),
"assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field
Value("/"),
),
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
),
default=Value(None),
output_field=models.CharField(),
)
)
.annotate(display_name=F("assignees__display_name"))
.values(
"first_name",
+15 -5
View File
@@ -102,6 +102,7 @@ class CycleViewSet(BaseViewSet):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -113,6 +114,7 @@ class CycleViewSet(BaseViewSet):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -142,6 +144,11 @@ class CycleViewSet(BaseViewSet):
distinct=True,
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
)
& (
Q(
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -488,12 +495,9 @@ class CycleViewSet(BaseViewSet):
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Delete the cycle
# TODO: Soft delete the cycle break the onetoone relationship with cycle issue
cycle.delete()
# Delete the cycle issues
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk"),
).delete()
# Delete the user favorite cycle
UserFavorite.objects.filter(
user=request.user,
@@ -607,6 +611,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -617,6 +622,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -627,6 +633,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -637,6 +644,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -647,6 +655,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -1210,6 +1219,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
+8 -2
View File
@@ -3,7 +3,7 @@ import json
# Django imports
from django.core import serializers
from django.db.models import F, Func, OuterRef, Q
from django.db.models import F, Func, OuterRef, Q, Subquery
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -102,7 +102,13 @@ class CycleIssueViewSet(BaseViewSet):
"issue_cycle__cycle",
)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
+43 -10
View File
@@ -42,6 +42,7 @@ from plane.db.models import (
Project,
Widget,
WorkspaceMember,
CycleIssue,
)
from plane.utils.issue_filters import issue_filters
@@ -191,7 +192,13 @@ def dashboard_assigned_issues(self, request, slug):
).select_related("issue"),
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -218,7 +225,10 @@ def dashboard_assigned_issues(self, request, slug):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -226,8 +236,11 @@ def dashboard_assigned_issues(self, request, slug):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -235,7 +248,11 @@ def dashboard_assigned_issues(self, request, slug):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -355,7 +372,13 @@ def dashboard_created_issues(self, request, slug):
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -382,7 +405,10 @@ def dashboard_created_issues(self, request, slug):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -390,8 +416,11 @@ def dashboard_created_issues(self, request, slug):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -399,7 +428,11 @@ def dashboard_created_issues(self, request, slug):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
+65 -16
View File
@@ -3,7 +3,7 @@ import json
# Django import
from django.utils import timezone
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch, Subquery
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
@@ -26,6 +26,7 @@ from plane.db.models import (
FileAsset,
Project,
ProjectMember,
CycleIssue,
)
from plane.app.serializers import (
IssueCreateSerializer,
@@ -112,7 +113,13 @@ class InboxIssueViewSet(BaseViewSet):
),
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -141,7 +148,10 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -149,8 +159,11 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -158,8 +171,11 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -186,7 +202,10 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
)
@@ -298,7 +317,12 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(
issue__label_issue__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -306,7 +330,10 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True),
filter=~Q(issue__assignees__id__isnull=True)
& Q(
issue__assignees__member_project__is_active=True
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -359,7 +386,10 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -367,7 +397,10 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -490,7 +523,12 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(
issue__label_issue__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -498,7 +536,12 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True),
filter=Q(
~Q(issue__assignees__id__isnull=True)
& Q(
issue__issue_assignee__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -543,7 +586,10 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__labels__id",
distinct=True,
filter=~Q(issue__labels__id__isnull=True),
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -551,7 +597,10 @@ class InboxIssueViewSet(BaseViewSet):
ArrayAgg(
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True),
filter=Q(
~Q(issue__assignees__id__isnull=True)
& Q(issue__issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
+9 -9
View File
@@ -3,14 +3,7 @@ import json
# Django imports
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import (
F,
Func,
OuterRef,
Q,
Prefetch,
Exists,
)
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -34,6 +27,7 @@ from plane.db.models import (
IssueLink,
IssueSubscriber,
IssueReaction,
CycleIssue
)
from plane.utils.grouper import (
issue_group_values,
@@ -71,7 +65,13 @@ class IssueArchiveViewSet(BaseViewSet):
.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(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -20,6 +20,7 @@ from plane.db.models import FileAsset, Workspace
from plane.bgtasks.issue_activities_task import issue_activity
from plane.app.permissions import allow_permission, ROLE
from plane.settings.storage import S3Storage
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
class IssueAttachmentEndpoint(BaseAPIView):
@@ -254,10 +255,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
issue_attachment.is_uploaded = True
# Get the storage metadata
if issue_attachment.storage_metadata is None:
storage = S3Storage(request=request)
issue_attachment.storage_metadata = storage.get_object_metadata(
issue_attachment.asset.name
)
if not issue_attachment.storage_metadata:
get_asset_object_metadata.delay(str(issue_attachment.id))
issue_attachment.save()
return Response(status=status.HTTP_204_NO_CONTENT)
+76 -19
View File
@@ -14,6 +14,7 @@ from django.db.models import (
Q,
UUIDField,
Value,
Subquery,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
@@ -42,6 +43,7 @@ from plane.db.models import (
IssueSubscriber,
Project,
ProjectMember,
CycleIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -83,7 +85,13 @@ class IssueListEndpoint(BaseAPIView):
.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(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -207,7 +215,13 @@ class IssueViewSet(BaseViewSet):
.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(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -471,7 +485,10 @@ class IssueViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -479,8 +496,11 @@ class IssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -488,8 +508,11 @@ class IssueViewSet(BaseViewSet):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -568,7 +591,10 @@ class IssueViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -576,8 +602,11 @@ class IssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -585,7 +614,11 @@ class IssueViewSet(BaseViewSet):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -748,13 +781,28 @@ class IssuePaginatedViewSet(BaseViewSet):
"workspace", "project", "state", "parent"
)
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
@@ -779,7 +827,7 @@ class IssuePaginatedViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
cursor = request.GET.get("cursor", None)
is_description_required = request.GET.get("description", False)
is_description_required = request.GET.get("description", "false")
updated_at = request.GET.get("updated_at__gt", None)
# required fields
@@ -812,7 +860,7 @@ class IssuePaginatedViewSet(BaseViewSet):
"sub_issues_count",
]
if is_description_required:
if str(is_description_required).lower() == "true":
required_fields.append("description_html")
# querying issues
@@ -846,7 +894,10 @@ class IssuePaginatedViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -854,8 +905,11 @@ class IssuePaginatedViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -863,8 +917,11 @@ class IssuePaginatedViewSet(BaseViewSet):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
+28 -6
View File
@@ -3,7 +3,16 @@ import json
# Django imports
from django.utils import timezone
from django.db.models import Q, OuterRef, F, Func, UUIDField, Value, CharField
from django.db.models import (
Q,
OuterRef,
F,
Func,
UUIDField,
Value,
CharField,
Subquery,
)
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models.functions import Coalesce
from django.contrib.postgres.aggregates import ArrayAgg
@@ -26,6 +35,7 @@ from plane.db.models import (
Issue,
FileAsset,
IssueLink,
CycleIssue,
)
from plane.bgtasks.issue_activities_task import issue_activity
@@ -83,7 +93,13 @@ class IssueRelationViewSet(BaseViewSet):
Issue.issue_objects.filter(workspace__slug=slug)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -112,7 +128,10 @@ class IssueRelationViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& (Q(label_issue__deleted_at__isnull=True))
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -120,8 +139,11 @@ class IssueRelationViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -268,7 +290,7 @@ class IssueRelationViewSet(BaseViewSet):
IssueRelationSerializer(issue_relation).data,
cls=DjangoJSONEncoder,
)
issue_relation.delete(soft=False)
issue_relation.delete()
issue_activity.delay(
type="issue_relation.activity.deleted",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
+23 -13
View File
@@ -3,14 +3,7 @@ import json
# Django imports
from django.utils import timezone
from django.db.models import (
OuterRef,
Func,
F,
Q,
Value,
UUIDField,
)
from django.db.models import OuterRef, Func, F, Q, Value, UUIDField, Subquery
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.contrib.postgres.aggregates import ArrayAgg
@@ -29,6 +22,7 @@ from plane.db.models import (
Issue,
IssueLink,
FileAsset,
CycleIssue,
)
from plane.bgtasks.issue_activities_task import issue_activity
from plane.utils.user_timezone_converter import user_timezone_converter
@@ -48,7 +42,13 @@ class SubIssuesEndpoint(BaseAPIView):
)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -77,7 +77,10 @@ class SubIssuesEndpoint(BaseAPIView):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -85,8 +88,11 @@ class SubIssuesEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -94,7 +100,11 @@ class SubIssuesEndpoint(BaseAPIView):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
+9 -7
View File
@@ -1,12 +1,7 @@
# Python imports
import json
from django.db.models import (
F,
Func,
OuterRef,
Q,
)
from django.db.models import F, Func, OuterRef, Q, Subquery
# Django Imports
from django.utils import timezone
@@ -28,6 +23,7 @@ from plane.db.models import (
IssueLink,
ModuleIssue,
Project,
CycleIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -65,7 +61,13 @@ class ModuleIssueViewSet(BaseViewSet):
)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
+34 -25
View File
@@ -42,47 +42,56 @@ class IssueSearchEndpoint(BaseAPIView):
issues = search_issues(query, issues)
if parent == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id)
)
issue = Issue.issue_objects.filter(pk=issue_id).first()
if issue:
issues = issues.filter(
~Q(pk=issue_id),
~Q(pk=issue.parent_id),
~Q(parent_id=issue_id),
)
if issue_relation == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id),
~Q(
issue_related__issue=issue,
issue_related__deleted_at__isnull=True,
),
~Q(
issue_relation__related_issue=issue,
issue_related__deleted_at__isnull=True,
),
)
issue = Issue.issue_objects.filter(pk=issue_id).first()
if issue:
issues = issues.filter(
~Q(pk=issue_id),
~(
Q(issue_related__issue=issue)
& Q(issue_related__deleted_at__isnull=True)
),
~(
Q(issue_relation__related_issue=issue)
& Q(issue_relation__deleted_at__isnull=True)
),
)
if sub_issue == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
issue = Issue.issue_objects.filter(pk=issue_id).first()
if issue:
issues = issues.filter(~Q(pk=issue_id), parent__isnull=True)
if issue.parent:
issues = issues.filter(~Q(pk=issue.parent_id))
if cycle == "true":
issues = issues.exclude(issue_cycle__isnull=False)
issues = issues.exclude(
Q(issue_cycle__isnull=False)
& Q(issue_cycle__deleted_at__isnull=True)
)
if module:
issues = issues.exclude(issue_module__module=module)
issues = issues.exclude(
Q(issue_module__module=module)
& Q(issue_module__deleted_at__isnull=True)
)
if target_date == "none":
issues = issues.filter(target_date__isnull=True)
if ProjectMember.objects.filter(
project_id=project_id,
member=self.request.user,
is_active=True,
role=5
role=5,
).exists():
issues = issues.filter(
created_by=self.request.user
)
issues = issues.filter(created_by=self.request.user)
return Response(
issues.values(
+33 -8
View File
@@ -9,6 +9,7 @@ from django.db.models import (
Q,
UUIDField,
Value,
Subquery,
)
from django.db.models.functions import Coalesce
from django.utils.decorators import method_decorator
@@ -36,6 +37,7 @@ from plane.db.models import (
WorkspaceMember,
ProjectMember,
Project,
CycleIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -205,7 +207,13 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -234,7 +242,10 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -242,8 +253,11 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -251,8 +265,11 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True),
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -271,7 +288,13 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
issue_queryset = (
self.get_queryset()
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
)
# check for the project member role, if the role is 5 then check for the guest_view_all_features if it is true then show all the issues else show only the issues created by the user
@@ -530,7 +553,9 @@ class IssueViewViewSet(BaseViewSet):
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView)
@allow_permission(
allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView
)
def destroy(self, request, slug, project_id, pk):
project_view = IssueView.objects.get(
pk=pk,
@@ -43,6 +43,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -53,6 +54,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -63,6 +65,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -73,6 +76,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -83,6 +87,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
+40 -22
View File
@@ -8,10 +8,11 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import (
F,
Q,
UUIDField,
Value,
Subquery,
OuterRef,
)
from django.db.models.functions import Coalesce
from django.utils.decorators import method_decorator
@@ -34,9 +35,9 @@ from plane.db.models import (
DraftIssue,
CycleIssue,
ModuleIssue,
DraftIssueModule,
DraftIssueCycle,
Workspace,
FileAsset,
)
from .. import BaseViewSet
from plane.bgtasks.issue_activities_task import issue_activity
@@ -53,13 +54,22 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
.prefetch_related(
"assignees", "labels", "draft_issue_module__module"
)
.annotate(cycle_id=F("draft_issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
DraftIssueCycle.objects.filter(
draft_issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& (Q(draft_label_issue__deleted_at__isnull=True))
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -67,8 +77,11 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(draft_issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -76,9 +89,12 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
ArrayAgg(
"draft_issue_module__module_id",
distinct=True,
filter=~Q(draft_issue_module__module_id__isnull=True)
& Q(
draft_issue_module__module__archived_at__isnull=True
filter=Q(
~Q(draft_issue_module__module_id__isnull=True)
& Q(
draft_issue_module__module__archived_at__isnull=True
)
& Q(draft_issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -259,9 +275,9 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
origin=request.META.get("HTTP_ORIGIN"),
)
if draft_issue.cycle_id:
if request.data.get("cycle_id", None):
created_records = CycleIssue.objects.create(
cycle_id=draft_issue.cycle_id,
cycle_id=request.data.get("cycle_id", None),
issue_id=serializer.data.get("id", None),
project_id=draft_issue.project_id,
workspace_id=draft_issue.workspace_id,
@@ -279,7 +295,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
{
"updated_cycle_issues": None,
"created_cycle_issues": serializers.serialize(
"json", created_records
"json", [created_records]
),
}
),
@@ -288,7 +304,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
origin=request.META.get("HTTP_ORIGIN"),
)
if draft_issue.module_ids:
if request.data.get("module_ids", []):
# bulk create the module
ModuleIssue.objects.bulk_create(
[
@@ -300,11 +316,11 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
created_by_id=draft_issue.created_by_id,
updated_by_id=draft_issue.updated_by_id,
)
for module in draft_issue.module_ids
for module in request.data.get("module_ids", [])
],
batch_size=10,
)
# Bulk Update the activity
# Update the activity
_ = [
issue_activity.delay(
type="module.activity.created",
@@ -317,18 +333,20 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
for module in draft_issue.module_ids
for module in request.data.get("module_ids", [])
]
# Update file assets
file_assets = FileAsset.objects.filter(draft_issue_id=draft_id)
file_assets.update(
issue_id=serializer.data.get("id", None),
entity_type=FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
draft_issue_id=None,
)
# delete the draft issue
draft_issue.delete()
# delete the draft issue module
DraftIssueModule.objects.filter(draft_issue=draft_issue).delete()
# delete the draft issue cycle
DraftIssueCycle.objects.filter(draft_issue=draft_issue).delete()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+25 -4
View File
@@ -3,7 +3,11 @@ from django.db.models import (
CharField,
Count,
Q,
OuterRef,
Subquery,
IntegerField,
)
from django.db.models.functions import Coalesce
from django.db.models.functions import Cast
# Third party modules
@@ -34,6 +38,7 @@ from plane.db.models import (
User,
Workspace,
WorkspaceMember,
DraftIssue,
)
from plane.utils.cache import cache_response, invalidate_cache
@@ -283,10 +288,26 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
class WorkspaceMemberUserEndpoint(BaseAPIView):
def get(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
member=request.user,
workspace__slug=slug,
is_active=True,
draft_issue_count = (
DraftIssue.objects.filter(
created_by=request.user,
workspace_id=OuterRef("workspace_id"),
)
.values("workspace_id")
.annotate(count=Count("id"))
.values("count")
)
workspace_member = (
WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True
)
.annotate(
draft_issue_count=Coalesce(
Subquery(draft_issue_count, output_field=IntegerField()), 0
)
)
.first()
)
serializer = WorkspaceMemberMeSerializer(workspace_member)
return Response(serializer.data, status=status.HTTP_200_OK)
+8 -1
View File
@@ -14,6 +14,7 @@ from django.db.models import (
Q,
Value,
When,
Subquery,
)
from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractWeek
@@ -120,7 +121,13 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -130,7 +130,9 @@ def get_label_details(slug, filters):
"""Fetch label details if required"""
return (
Issue.objects.filter(
workspace__slug=slug, **filters, labels__id__isnull=False
workspace__slug=slug,
**filters,
labels__id__isnull=False & Q(label_issue__deleted_at__isnull=True),
)
.distinct("labels__id")
.order_by("labels__id")
+2 -2
View File
@@ -21,8 +21,8 @@ def soft_delete_related_objects(
try:
# Check if the field has CASCADE on delete
if (
hasattr(field.remote_field, "on_delete")
and field.remote_field.on_delete == models.CASCADE
not hasattr(field.remote_field, "on_delete")
or field.remote_field.on_delete == models.CASCADE
):
if field.one_to_many:
related_objects = getattr(instance, field.name).all()
@@ -0,0 +1,28 @@
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import FileAsset
from plane.settings.storage import S3Storage
from plane.utils.exception_logger import log_exception
@shared_task
def get_asset_object_metadata(asset_id):
try:
# Get the asset
asset = FileAsset.objects.get(pk=asset_id)
# Create an instance of the S3 storage
storage = S3Storage()
# Get the storage
asset.storage_metadata = storage.get_object_metadata(
object_name=asset.asset.name
)
# Save the asset
asset.save()
return
except FileAsset.DoesNotExist:
return
except Exception as e:
log_exception(e)
return
+4 -4
View File
@@ -32,14 +32,14 @@ app.conf.beat_schedule = {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
"schedule": crontab(minute="*/5"),
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=0, minute=0),
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6"),
@@ -68,7 +68,6 @@ class Command(BaseCommand):
Key="test_permission_check.txt",
Body=b"Test",
)
self.stdout.write("PutObject permission granted.")
permissions["s3:PutObject"] = True
# Clean up
except ClientError as e:
@@ -150,23 +149,47 @@ class Command(BaseCommand):
def handle(self, *args, **options):
# Create a session using the credentials from Django settings
try:
# Check if the bucket exists
s3_client = self.get_s3_client()
# Get the bucket name from the environment
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
self.stdout.write(self.style.NOTICE("Checking bucket..."))
# Check if the bucket exists
s3_client.head_bucket(Bucket=bucket_name)
# If the bucket exists, print a success message
# Check if the bucket exists
s3_client = self.get_s3_client()
# Get the bucket name from the environment
bucket_name = os.environ.get("AWS_S3_BUCKET_NAME")
if not bucket_name:
self.stdout.write(
self.style.SUCCESS(f"Bucket '{bucket_name}' exists.")
self.style.ERROR(
"Please set the AWS_S3_BUCKET_NAME environment variable."
)
)
return
self.stdout.write(self.style.NOTICE("Checking bucket..."))
# Check if the bucket exists
try:
s3_client.head_bucket(Bucket=bucket_name)
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code == "404":
self.stdout.write(
self.style.ERROR(f"Bucket '{bucket_name}' does not exist.")
)
return
else:
self.stdout.write(f"Error: {e}")
# If the bucket exists, print a success message
self.stdout.write(
self.style.SUCCESS(f"Bucket '{bucket_name}' exists.")
)
try:
# Check the permissions of the access key
permissions = self.check_s3_permissions(bucket_name)
except ClientError as e:
self.stdout.write(f"Error: {e}")
except Exception as e:
self.stdout.write(f"Error: {e}")
# If the access key has the required permissions
try:
if all(permissions.values()):
self.stdout.write(
self.style.SUCCESS(
@@ -175,35 +198,26 @@ class Command(BaseCommand):
)
# Making the existing objects public
self.make_objects_public(bucket_name)
# If the access key does not have PutBucketPolicy permission
# write the bucket policy to a file
if (
all(
{
k: v
for k, v in permissions.items()
if k != "s3:PutBucketPolicy"
}.values()
)
and not permissions["s3:PutBucketPolicy"]
):
self.stdout.write(
self.style.WARNING(
"Access key does not have PutBucketPolicy permission."
)
)
# Writing to a file
with open("permissions.json", "w") as f:
f.write(
json.dumps(self.generate_bucket_policy(bucket_name))
)
self.stdout.write(
self.style.WARNING(
"Permissions have been written to permissions.json."
)
)
return
except Exception as ex:
# Handle any other exception
self.stdout.write(self.style.ERROR(f"An error occurred: {ex}"))
except Exception as e:
self.stdout.write(f"Error: {e}")
# write the bucket policy to a file
self.stdout.write(
self.style.WARNING(
"Generating permissions.json for manual bucket policy update."
)
)
try:
# Writing to a file
with open("permissions.json", "w") as f:
f.write(json.dumps(self.generate_bucket_policy(bucket_name)))
self.stdout.write(
self.style.WARNING(
"Permissions have been written to permissions.json."
)
)
return
except IOError as e:
self.stdout.write(f"Error writing permissions.json: {e}")
return
@@ -1,179 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-14 07:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("db", "0080_fileasset_draft_issue_alter_fileasset_entity_type"),
]
operations = [
migrations.AddField(
model_name="issuetype",
name="external_id",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="issuetype",
name="external_source",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.RenameField(
model_name="usernotificationpreference",
old_name="property_change",
new_name="email_property_change",
),
migrations.AlterField(
model_name="usernotificationpreference",
name="email_property_change",
field=models.BooleanField(default=False),
),
migrations.RenameField(
model_name="usernotificationpreference",
old_name="state_change",
new_name="email_state_change",
),
migrations.AlterField(
model_name="usernotificationpreference",
name="email_state_change",
field=models.BooleanField(default=False),
),
migrations.RenameField(
model_name="usernotificationpreference",
old_name="comment",
new_name="email_comment",
),
migrations.AlterField(
model_name="usernotificationpreference",
name="email_comment",
field=models.BooleanField(default=False),
),
migrations.RenameField(
model_name="usernotificationpreference",
old_name="mention",
new_name="email_mention",
),
migrations.AlterField(
model_name="usernotificationpreference",
name="email_mention",
field=models.BooleanField(default=False),
),
migrations.RenameField(
model_name="usernotificationpreference",
old_name="issue_completed",
new_name="email_issue_completed",
),
migrations.AlterField(
model_name="usernotificationpreference",
name="email_issue_completed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="deployboard",
name="is_disabled",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="email_assignee_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="email_cycle_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="email_module_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="email_priority_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="email_reactions",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="email_start_target_date_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_assignee_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_comment",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_cycle_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_issue_completed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_mention",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_module_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_priority_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_property_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_reactions",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_start_target_date_change",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="usernotificationpreference",
name="in_app_state_change",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="deployboard",
name="entity_name",
field=models.CharField(
choices=[
("project", "Project"),
("issue", "Issue"),
("module", "Module"),
("cycle", "Task"),
("page", "Page"),
("view", "View"),
("inbox", "Inbox"),
],
max_length=30,
),
),
]
@@ -0,0 +1,187 @@
# Generated by Django 4.2.16 on 2024-10-15 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("db", "0080_fileasset_draft_issue_alter_fileasset_entity_type"),
]
operations = [
migrations.RemoveField(
model_name="globalview",
name="created_by",
),
migrations.RemoveField(
model_name="globalview",
name="updated_by",
),
migrations.RemoveField(
model_name="globalview",
name="workspace",
),
migrations.AlterUniqueTogether(
name="issueviewfavorite",
unique_together=None,
),
migrations.RemoveField(
model_name="issueviewfavorite",
name="created_by",
),
migrations.RemoveField(
model_name="issueviewfavorite",
name="project",
),
migrations.RemoveField(
model_name="issueviewfavorite",
name="updated_by",
),
migrations.RemoveField(
model_name="issueviewfavorite",
name="user",
),
migrations.RemoveField(
model_name="issueviewfavorite",
name="view",
),
migrations.RemoveField(
model_name="issueviewfavorite",
name="workspace",
),
migrations.AlterUniqueTogether(
name="modulefavorite",
unique_together=None,
),
migrations.RemoveField(
model_name="modulefavorite",
name="created_by",
),
migrations.RemoveField(
model_name="modulefavorite",
name="module",
),
migrations.RemoveField(
model_name="modulefavorite",
name="project",
),
migrations.RemoveField(
model_name="modulefavorite",
name="updated_by",
),
migrations.RemoveField(
model_name="modulefavorite",
name="user",
),
migrations.RemoveField(
model_name="modulefavorite",
name="workspace",
),
migrations.RemoveField(
model_name="pageblock",
name="created_by",
),
migrations.RemoveField(
model_name="pageblock",
name="issue",
),
migrations.RemoveField(
model_name="pageblock",
name="page",
),
migrations.RemoveField(
model_name="pageblock",
name="project",
),
migrations.RemoveField(
model_name="pageblock",
name="updated_by",
),
migrations.RemoveField(
model_name="pageblock",
name="workspace",
),
migrations.AlterUniqueTogether(
name="pagefavorite",
unique_together=None,
),
migrations.RemoveField(
model_name="pagefavorite",
name="created_by",
),
migrations.RemoveField(
model_name="pagefavorite",
name="page",
),
migrations.RemoveField(
model_name="pagefavorite",
name="project",
),
migrations.RemoveField(
model_name="pagefavorite",
name="updated_by",
),
migrations.RemoveField(
model_name="pagefavorite",
name="user",
),
migrations.RemoveField(
model_name="pagefavorite",
name="workspace",
),
migrations.AlterUniqueTogether(
name="projectfavorite",
unique_together=None,
),
migrations.RemoveField(
model_name="projectfavorite",
name="created_by",
),
migrations.RemoveField(
model_name="projectfavorite",
name="project",
),
migrations.RemoveField(
model_name="projectfavorite",
name="updated_by",
),
migrations.RemoveField(
model_name="projectfavorite",
name="user",
),
migrations.RemoveField(
model_name="projectfavorite",
name="workspace",
),
migrations.AddField(
model_name="issuetype",
name="external_id",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="issuetype",
name="external_source",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.DeleteModel(
name="CycleFavorite",
),
migrations.DeleteModel(
name="GlobalView",
),
migrations.DeleteModel(
name="IssueViewFavorite",
),
migrations.DeleteModel(
name="ModuleFavorite",
),
migrations.DeleteModel(
name="PageBlock",
),
migrations.DeleteModel(
name="PageFavorite",
),
migrations.DeleteModel(
name="ProjectFavorite",
),
]
@@ -0,0 +1,63 @@
# Generated by Django 4.2.15 on 2024-10-22 08:00
from django.db import migrations, models
import django.db.models.deletion
import django.db.models.manager
class Migration(migrations.Migration):
dependencies = [
("db", "0081_remove_globalview_created_by_and_more"),
]
operations = [
migrations.AlterModelManagers(
name="issue",
managers=[
("issue_objects", django.db.models.manager.Manager()),
],
),
migrations.AlterField(
model_name="cycleissue",
name="issue",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_cycle",
to="db.issue",
),
),
migrations.AlterField(
model_name="draftissuecycle",
name="draft_issue",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="draft_issue_cycle",
to="db.draftissue",
),
),
migrations.AlterUniqueTogether(
name="cycleissue",
unique_together={("issue", "cycle", "deleted_at")},
),
migrations.AlterUniqueTogether(
name="draftissuecycle",
unique_together={("draft_issue", "cycle", "deleted_at")},
),
migrations.AddConstraint(
model_name="cycleissue",
constraint=models.UniqueConstraint(
condition=models.Q(("deleted_at__isnull", True)),
fields=("cycle", "issue"),
name="cycle_issue_when_deleted_at_null",
),
),
migrations.AddConstraint(
model_name="draftissuecycle",
constraint=models.UniqueConstraint(
condition=models.Q(("deleted_at__isnull", True)),
fields=("draft_issue", "cycle"),
name="draft_issue_cycle_when_deleted_at_null",
),
),
]
+11 -1
View File
@@ -43,9 +43,19 @@ class UserAuditModel(models.Model):
abstract = True
class SoftDeletionQuerySet(models.QuerySet):
def delete(self, soft=True):
if soft:
return self.update(deleted_at=timezone.now())
else:
return super().delete()
class SoftDeletionManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
return SoftDeletionQuerySet(self.model, using=self._db).filter(
deleted_at__isnull=True
)
class SoftDeleteModel(models.Model):
+3 -6
View File
@@ -2,7 +2,7 @@ from .analytic import AnalyticView
from .api import APIActivityLog, APIToken
from .asset import FileAsset
from .base import BaseModel
from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties
from .cycle import Cycle, CycleIssue, CycleUserProperties
from .dashboard import Dashboard, DashboardWidget, Widget
from .deploy_board import DeployBoard
from .draft import DraftIssue, DraftIssueAssignee, DraftIssueLabel, DraftIssueModule, DraftIssueCycle
@@ -39,7 +39,6 @@ from .issue import (
)
from .module import (
Module,
ModuleFavorite,
ModuleIssue,
ModuleLink,
ModuleMember,
@@ -52,7 +51,6 @@ from .notification import (
)
from .page import (
Page,
PageFavorite,
PageLabel,
PageLog,
ProjectPage,
@@ -61,7 +59,6 @@ from .page import (
from .project import (
Project,
ProjectBaseModel,
ProjectFavorite,
ProjectIdentifier,
ProjectMember,
ProjectMemberInvite,
@@ -72,7 +69,7 @@ from .session import Session
from .social_connection import SocialLoginConnection
from .state import State
from .user import Account, Profile, User
from .view import IssueView, IssueViewFavorite
from .view import IssueView
from .webhook import Webhook, WebhookLog
from .workspace import (
Team,
@@ -87,7 +84,7 @@ from .workspace import (
from .importer import Importer
from .page import Page, PageLog, PageFavorite, PageLabel
from .page import Page, PageLog, PageLabel
from .estimate import Estimate, EstimatePoint
+9 -28
View File
@@ -110,7 +110,7 @@ class CycleIssue(ProjectBaseModel):
Cycle Issues
"""
issue = models.OneToOneField(
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_cycle"
)
cycle = models.ForeignKey(
@@ -118,6 +118,14 @@ class CycleIssue(ProjectBaseModel):
)
class Meta:
unique_together = ["issue", "cycle", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["cycle", "issue"],
condition=models.Q(deleted_at__isnull=True),
name="cycle_issue_when_deleted_at_null",
)
]
verbose_name = "Cycle Issue"
verbose_name_plural = "Cycle Issues"
db_table = "cycle_issues"
@@ -127,33 +135,6 @@ class CycleIssue(ProjectBaseModel):
return f"{self.cycle}"
# DEPRECATED TODO: - Remove in next release
class CycleFavorite(ProjectBaseModel):
"""_summary_
CycleFavorite (model): To store all the cycle favorite of the user
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="cycle_favorites",
)
cycle = models.ForeignKey(
"db.Cycle", on_delete=models.CASCADE, related_name="cycle_favorites"
)
class Meta:
unique_together = ["cycle", "user"]
verbose_name = "Cycle Favorite"
verbose_name_plural = "Cycle Favorites"
db_table = "cycle_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the cycle"""
return f"{self.user.email} <{self.cycle.name}>"
class CycleUserProperties(ProjectBaseModel):
cycle = models.ForeignKey(
"db.Cycle",
@@ -20,7 +20,6 @@ class DeployBoard(WorkspaceBaseModel):
("cycle", "Task"),
("page", "Page"),
("view", "View"),
("inbox", "Inbox"),
)
entity_identifier = models.UUIDField(null=True)
@@ -42,7 +41,6 @@ class DeployBoard(WorkspaceBaseModel):
is_votes_enabled = models.BooleanField(default=False)
view_props = models.JSONField(default=dict)
is_activity_enabled = models.BooleanField(default=True)
is_disabled = models.BooleanField(default=False)
def __str__(self):
"""Return name of the deploy board"""
+9 -1
View File
@@ -234,7 +234,7 @@ class DraftIssueCycle(WorkspaceBaseModel):
Draft Issue Cycles
"""
draft_issue = models.OneToOneField(
draft_issue = models.ForeignKey(
"db.DraftIssue",
on_delete=models.CASCADE,
related_name="draft_issue_cycle",
@@ -244,6 +244,14 @@ class DraftIssueCycle(WorkspaceBaseModel):
)
class Meta:
unique_together = ["draft_issue", "cycle", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["draft_issue", "cycle"],
condition=models.Q(deleted_at__isnull=True),
name="draft_issue_cycle_when_deleted_at_null",
)
]
verbose_name = "Draft Issue Cycle"
verbose_name_plural = "Draft Issue Cycles"
db_table = "draft_issue_cycles"
+2 -2
View File
@@ -12,6 +12,7 @@ from django.db.models import Q
# Module imports
from plane.utils.html_processor import strip_tags
from plane.db.mixins import SoftDeletionManager
from .project import ProjectBaseModel
@@ -79,7 +80,7 @@ def get_default_display_properties():
# TODO: Handle identifiers for Bulk Inserts - nk
class IssueManager(models.Manager):
class IssueManager(SoftDeletionManager):
def get_queryset(self):
return (
super()
@@ -172,7 +173,6 @@ class Issue(ProjectBaseModel):
blank=True,
)
objects = models.Manager()
issue_objects = IssueManager()
class Meta:
+2 -29
View File
@@ -100,9 +100,9 @@ class Module(ProjectBaseModel):
unique_together = ["name", "project", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=['name', 'project'],
fields=["name", "project"],
condition=Q(deleted_at__isnull=True),
name='module_unique_name_project_when_deleted_at_null'
name="module_unique_name_project_when_deleted_at_null",
)
]
verbose_name = "Module"
@@ -191,33 +191,6 @@ class ModuleLink(ProjectBaseModel):
return f"{self.module.name} {self.url}"
# DEPRECATED TODO: - Remove in next release
class ModuleFavorite(ProjectBaseModel):
"""_summary_
ModuleFavorite (model): To store all the module favorite of the user
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="module_favorites",
)
module = models.ForeignKey(
"db.Module", on_delete=models.CASCADE, related_name="module_favorites"
)
class Meta:
unique_together = ["module", "user"]
verbose_name = "Module Favorite"
verbose_name_plural = "Module Favorites"
db_table = "module_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the module"""
return f"{self.user.email} <{self.module.name}>"
class ModuleUserProperties(ProjectBaseModel):
module = models.ForeignKey(
"db.Module",
+7 -25
View File
@@ -6,6 +6,7 @@ from django.db import models
from .base import BaseModel
class Notification(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", related_name="notifications", on_delete=models.CASCADE
@@ -89,31 +90,12 @@ class UserNotificationPreference(BaseModel):
null=True,
)
# email preference fields
email_property_change = models.BooleanField(default=False)
email_state_change = models.BooleanField(default=False)
email_priority_change = models.BooleanField(default=False)
email_assignee_change = models.BooleanField(default=False)
email_start_target_date_change = models.BooleanField(default=False)
email_module_change = models.BooleanField(default=False)
email_cycle_change = models.BooleanField(default=False)
email_reactions = models.BooleanField(default=False)
email_comment = models.BooleanField(default=False)
email_mention = models.BooleanField(default=False)
email_issue_completed = models.BooleanField(default=False)
# in app preference fields
in_app_property_change = models.BooleanField(default=False)
in_app_state_change = models.BooleanField(default=False)
in_app_priority_change = models.BooleanField(default=False)
in_app_assignee_change = models.BooleanField(default=False)
in_app_start_target_date_change = models.BooleanField(default=False)
in_app_module_change = models.BooleanField(default=False)
in_app_cycle_change = models.BooleanField(default=False)
in_app_reactions = models.BooleanField(default=False)
in_app_comment = models.BooleanField(default=False)
in_app_mention = models.BooleanField(default=False)
in_app_issue_completed = models.BooleanField(default=False)
# preference fields
property_change = models.BooleanField(default=True)
state_change = models.BooleanField(default=True)
comment = models.BooleanField(default=True)
mention = models.BooleanField(default=True)
issue_completed = models.BooleanField(default=True)
class Meta:
verbose_name = "UserNotificationPreference"
-81
View File
@@ -9,7 +9,6 @@ from django.db import models
# Module imports
from plane.utils.html_processor import strip_tags
from .project import ProjectBaseModel
from .base import BaseModel
@@ -119,86 +118,6 @@ class PageLog(BaseModel):
return f"{self.page.name} {self.entity_name}"
# DEPRECATED TODO: - Remove in next release
class PageBlock(ProjectBaseModel):
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, related_name="blocks"
)
name = models.CharField(max_length=255)
description = models.JSONField(default=dict, blank=True)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
issue = models.ForeignKey(
"db.Issue", on_delete=models.SET_NULL, related_name="blocks", null=True
)
completed_at = models.DateTimeField(null=True)
sort_order = models.FloatField(default=65535)
sync = models.BooleanField(default=True)
def save(self, *args, **kwargs):
if self._state.adding:
largest_sort_order = PageBlock.objects.filter(
project=self.project, page=self.page
).aggregate(largest=models.Max("sort_order"))["largest"]
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
# 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)
)
if self.completed_at and self.issue:
try:
from plane.db.models import Issue, State
completed_state = State.objects.filter(
group="completed", project=self.project
).first()
if completed_state is not None:
Issue.objects.update(
pk=self.issue_id, state=completed_state
)
except ImportError:
pass
super(PageBlock, self).save(*args, **kwargs)
class Meta:
verbose_name = "Page Block"
verbose_name_plural = "Page Blocks"
db_table = "page_blocks"
ordering = ("-created_at",)
def __str__(self):
"""Return page and page block"""
return f"{self.page.name} <{self.name}>"
# DEPRECATED TODO: - Remove in next release
class PageFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="page_favorites",
)
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, related_name="page_favorites"
)
class Meta:
unique_together = ["page", "user"]
verbose_name = "Page Favorite"
verbose_name_plural = "Page Favorites"
db_table = "page_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the page"""
return f"{self.user.email} <{self.page.name}>"
class PageLabel(BaseModel):
label = models.ForeignKey(
"db.Label", on_delete=models.CASCADE, related_name="page_labels"
+3 -21
View File
@@ -181,7 +181,9 @@ class ProjectBaseModel(BaseModel):
Project, on_delete=models.CASCADE, related_name="project_%(class)s"
)
workspace = models.ForeignKey(
"db.Workspace", models.CASCADE, related_name="workspace_%(class)s"
"db.Workspace",
on_delete=models.CASCADE,
related_name="workspace_%(class)s",
)
class Meta:
@@ -285,26 +287,6 @@ class ProjectIdentifier(AuditModel):
ordering = ("-created_at",)
# DEPRECATED TODO: - Remove in next release
class ProjectFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="project_favorites",
)
class Meta:
unique_together = ["project", "user"]
verbose_name = "Project Favorite"
verbose_name_plural = "Project Favorites"
db_table = "project_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user of the project"""
return f"{self.user.email} <{self.project.name}>"
def get_anchor():
return uuid4().hex
+5
View File
@@ -252,4 +252,9 @@ def create_user_notification(sender, instance, created, **kwargs):
UserNotificationPreference.objects.create(
user=instance,
property_change=False,
state_change=False,
comment=False,
mention=False,
issue_completed=False,
)
-61
View File
@@ -3,8 +3,6 @@ from django.conf import settings
from django.db import models
# Module import
from .base import BaseModel
from .project import ProjectBaseModel
from .workspace import WorkspaceBaseModel
from plane.utils.issue_filters import issue_filters
@@ -52,41 +50,6 @@ def get_default_display_properties():
"updated_on": True,
}
# DEPRECATED TODO: - Remove in next release
class GlobalView(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
)
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query")
access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public"))
)
query_data = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
logo_props = models.JSONField(default=dict)
class Meta:
verbose_name = "Global View"
verbose_name_plural = "Global Views"
db_table = "global_views"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
if self._state.adding:
largest_sort_order = GlobalView.objects.filter(
workspace=self.workspace
).aggregate(largest=models.Max("sort_order"))["largest"]
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
super(GlobalView, self).save(*args, **kwargs)
def __str__(self):
"""Return name of the View"""
return f"{self.name} <{self.workspace.name}>"
class IssueView(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name")
@@ -109,7 +72,6 @@ class IssueView(WorkspaceBaseModel):
)
is_locked = models.BooleanField(default=False)
class Meta:
verbose_name = "Issue View"
verbose_name_plural = "Issue Views"
@@ -139,26 +101,3 @@ class IssueView(WorkspaceBaseModel):
def __str__(self):
"""Return name of the View"""
return f"{self.name} <{self.project.name}>"
# DEPRECATED TODO: - Remove in next release
class IssueViewFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="user_view_favorites",
)
view = models.ForeignKey(
"db.IssueView", on_delete=models.CASCADE, related_name="view_favorites"
)
class Meta:
unique_together = ["view", "user"]
verbose_name = "View Favorite"
verbose_name_plural = "View Favorites"
db_table = "view_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the view"""
return f"{self.user.email} <{self.view.name}>"
+5 -1
View File
@@ -39,7 +39,11 @@ class S3Storage(S3Boto3Storage):
aws_access_key_id=self.aws_access_key_id,
aws_secret_access_key=self.aws_secret_access_key,
region_name=self.aws_region,
endpoint_url=f"{request.scheme}://{request.get_host()}",
endpoint_url=(
f"{request.scheme}://{request.get_host()}"
if request
else self.aws_s3_endpoint_url
),
config=boto3.session.Config(signature_version="s3v4"),
)
else:
+10 -2
View File
@@ -35,8 +35,16 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
}
annotations_map = {
"assignee_ids": ("assignees__id", ~Q(assignees__id__isnull=True)),
"label_ids": ("labels__id", ~Q(labels__id__isnull=True)),
"assignee_ids": (
"assignees__id",
~Q(assignees__id__isnull=True)
& Q(issue_assignee__deleted_at__isnull=True),
),
"label_ids": (
"labels__id",
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
"module_ids": (
"issue_module__module_id",
~Q(issue_module__module_id__isnull=True),
+3 -5
View File
@@ -15,6 +15,7 @@ from rest_framework.response import Response
from .base import BaseAPIView
from plane.db.models import DeployBoard, FileAsset
from plane.settings.storage import S3Storage
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
class EntityAssetEndpoint(BaseAPIView):
@@ -159,14 +160,11 @@ class EntityAssetEndpoint(BaseAPIView):
# get the asset id
asset = FileAsset.objects.get(id=pk, workspace=deploy_board.workspace)
storage = S3Storage(request=request)
# get the storage metadata
asset.is_uploaded = True
# get the storage metadata
if asset.storage_metadata is None:
asset.storage_metadata = storage.get_object_metadata(
object_name=asset.asset.name
)
if not asset.storage_metadata:
get_asset_object_metadata.delay(str(asset.id))
# update the attributes
asset.attributes = request.data.get("attributes", asset.attributes)
+28 -6
View File
@@ -20,6 +20,7 @@ from django.db.models import (
OuterRef,
Func,
CharField,
Subquery,
)
from django.db.models.functions import Concat
@@ -62,6 +63,7 @@ from plane.db.models import (
IssueVote,
ProjectPublicMember,
FileAsset,
CycleIssue,
)
from plane.bgtasks.issue_activities_task import issue_activity
from plane.utils.issue_filters import issue_filters
@@ -106,7 +108,13 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
queryset=IssueVote.objects.select_related("actor"),
)
)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -695,13 +703,22 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -709,8 +726,11 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True),
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
@@ -718,7 +738,9 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
filter=~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
+2 -2
View File
@@ -138,7 +138,7 @@ def burndown_plot(
estimate__type="points",
).exists()
if estimate_type and plot_type == "points" and cycle_id:
issue_estimates = Issue.objects.filter(
issue_estimates = Issue.issue_objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_cycle__cycle_id=cycle_id,
@@ -149,7 +149,7 @@ def burndown_plot(
total_estimate_points = sum(issue_estimates)
if estimate_type and plot_type == "points" and module_id:
issue_estimates = Issue.objects.filter(
issue_estimates = Issue.issue_objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_module__module_id=module_id,
+11 -2
View File
@@ -25,13 +25,22 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
}
annotations_map = {
"assignee_ids": ("assignees__id", ~Q(assignees__id__isnull=True)),
"label_ids": ("labels__id", ~Q(labels__id__isnull=True)),
"assignee_ids": (
"assignees__id",
~Q(assignees__id__isnull=True)
& Q(issue_assignee__deleted_at__isnull=True),
),
"label_ids": (
"labels__id",
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
"module_ids": (
"issue_module__module_id",
(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
}
+22 -8
View File
@@ -48,14 +48,22 @@ def string_date_filter(
if term == "weeks":
if subsequent == "after":
if offset == "fromnow":
issue_filter[f"{date_filter}__gte"] = now + timedelta(weeks=duration)
issue_filter[f"{date_filter}__gte"] = now + timedelta(
weeks=duration
)
else:
issue_filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration)
issue_filter[f"{date_filter}__gte"] = now - timedelta(
weeks=duration
)
else:
if offset == "fromnow":
issue_filter[f"{date_filter}__lte"] = now + timedelta(weeks=duration)
issue_filter[f"{date_filter}__lte"] = now + timedelta(
weeks=duration
)
else:
issue_filter[f"{date_filter}__lte"] = now - timedelta(weeks=duration)
issue_filter[f"{date_filter}__lte"] = now - timedelta(
weeks=duration
)
def date_filter(issue_filter, date_term, queries):
@@ -120,7 +128,9 @@ def filter_state_group(params, issue_filter, method, prefix=""):
and len(params.get("state_group"))
and params.get("state_group") != "null"
):
issue_filter[f"{prefix}state__group__in"] = params.get("state_group")
issue_filter[f"{prefix}state__group__in"] = params.get(
"state_group"
)
return issue_filter
@@ -242,8 +252,8 @@ def filter_mentions(params, issue_filter, method, prefix=""):
and len(params.get("mentions"))
and params.get("mentions") != "null"
):
issue_filter[f"{prefix}issue_mention__mention__id__in"] = params.get(
"mentions"
issue_filter[f"{prefix}issue_mention__mention__id__in"] = (
params.get("mentions")
)
return issue_filter
@@ -411,7 +421,10 @@ def filter_cycle(params, issue_filter, method, prefix=""):
and len(params.get("cycle"))
and params.get("cycle") != "null"
):
issue_filter[f"{prefix}issue_cycle__cycle_id__in"] = params.get("cycle")
issue_filter[f"{prefix}issue_cycle__cycle_id__in"] = params.get(
"cycle"
)
issue_filter[f"{prefix}issue_cycle__deleted_at__isnull"] = True
return issue_filter
@@ -434,6 +447,7 @@ def filter_module(params, issue_filter, method, prefix=""):
issue_filter[f"{prefix}issue_module__module_id__in"] = params.get(
"module"
)
issue_filter[f"{prefix}issue_module__deleted_at__isnull"] = True
return issue_filter
+2 -1
View File
@@ -185,6 +185,7 @@ class OffsetPaginator:
class GroupedOffsetPaginator(OffsetPaginator):
# Field mappers - list m2m fields here
FIELD_MAPPER = {
"labels__id": "label_ids",
@@ -682,7 +683,7 @@ class SubGroupedOffsetPaginator(OffsetPaginator):
# for multi groups
result[
self.FIELD_MAPPER.get(self.sub_group_by_field_name)
] = [] if "None" in sub_group_ids else sub_group_ids
] = ([] if "None" in sub_group_ids else sub_group_ids)
# If a result belongs to multiple groups, add it to each group
processed_results[str(group_value)]["results"][
str(sub_group_value)
+1 -1
View File
@@ -34,7 +34,7 @@ x-app-env: &app-env
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
# DATA STORE SETTINGS
- USE_MINIO=${USE_MINIO:-1}
- AWS_REGION=${AWS_REGION:-""}
- AWS_REGION=${AWS_REGION:-}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-"access-key"}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-"secret-key"}
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
+2 -2
View File
@@ -60,12 +60,12 @@ http {
proxy_pass http://space:3002/spaces/;
}
location /${BUCKET_NAME}/ {
location /${BUCKET_NAME} {
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ${dollar}http_host;
proxy_pass http://plane-minio:9000/uploads/;
proxy_pass http://plane-minio:9000/${BUCKET_NAME};
}
}
}
+2 -2
View File
@@ -68,12 +68,12 @@ http {
proxy_pass http://space:3000/spaces/;
}
location /${BUCKET_NAME}/ {
location /${BUCKET_NAME} {
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ${dollar}http_host;
proxy_pass http://plane-minio:9000/uploads/;
proxy_pass http://plane-minio:9000/${BUCKET_NAME};
}
}
}
-3
View File
@@ -24,9 +24,6 @@
"prettier-plugin-tailwindcss": "^0.5.4",
"turbo": "^2.1.1"
},
"resolutions": {
"@types/react": "18.2.48"
},
"packageManager": "yarn@1.22.22",
"name": "plane"
}
+4 -5
View File
@@ -30,9 +30,8 @@
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"peerDependencies": {
"next": "12.3.2",
"react": "^18.2.0",
"react-dom": "18.2.0"
"react": "^18.3.1",
"react-dom": "18.3.1"
},
"dependencies": {
"@floating-ui/react": "^0.26.4",
@@ -76,8 +75,8 @@
"@plane/eslint-config": "*",
"@plane/typescript-config": "*",
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsup": "^7.2.0",
@@ -16,8 +16,8 @@ type Props = {
export const BubbleMenuColorSelector: FC<Props> = (props) => {
const { editor, isOpen, setIsOpen } = props;
const activeTextColor = COLORS_LIST.find((c) => editor.getAttributes("textStyle").color === c.key);
const activeBackgroundColor = COLORS_LIST.find((c) => editor.getAttributes("textStyle").backgroundColor === c.key);
const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive(c.key));
const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive(c.key));
return (
<div className="relative h-full">
@@ -211,7 +211,7 @@ export const ImageItem = (editor: Editor) =>
export const TextColorItem = (editor: Editor): EditorMenuItem => ({
key: "text-color",
name: "Color",
isActive: (color) => editor.getAttributes("textStyle").color === color,
isActive: (color) => editor.isActive("customColor", { color }),
command: (color: string) => toggleTextColor(color, editor),
icon: Palette,
});
@@ -219,7 +219,7 @@ export const TextColorItem = (editor: Editor): EditorMenuItem => ({
export const BackgroundColorItem = (editor: Editor): EditorMenuItem => ({
key: "background-color",
name: "Background color",
isActive: (color) => editor.getAttributes("textStyle").backgroundColor === color,
isActive: (color) => editor.isActive("customColor", { backgroundColor: color }),
command: (color: string) => toggleBackgroundColor(color, editor),
icon: Palette,
});
@@ -16,8 +16,7 @@ import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props
import { CustomMentionWithoutProps } from "./mentions/mentions-without-props";
import { CustomQuoteExtension } from "./quote";
import { TableHeader, TableCell, TableRow, Table } from "./table";
import { CustomTextColorExtension } from "./custom-text-color";
import { CustomBackgroundColorExtension } from "./custom-background-color";
import { CustomColorExtension } from "./custom-color";
export const CoreEditorExtensionsWithoutProps = [
StarterKit.configure({
@@ -85,8 +84,7 @@ export const CoreEditorExtensionsWithoutProps = [
TableCell,
TableRow,
CustomMentionWithoutProps(),
CustomTextColorExtension,
CustomBackgroundColorExtension,
CustomColorExtension,
];
export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()];
@@ -1,78 +0,0 @@
import { Extension } from "@tiptap/core";
// constants
import { COLORS_LIST } from "@/constants/common";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
backgroundColor: {
/**
* Set the background color
* @param color The color to set
* @example editor.commands.setBackgroundColor('red')
*/
setBackgroundColor: (color: string) => ReturnType;
/**
* Unset the background color
* @example editor.commands.unsetBackgroundColor()
*/
unsetBackgroundColor: () => ReturnType;
};
}
}
export const CustomBackgroundColorExtension = Extension.create({
name: "customBackgroundColor",
addOptions() {
return {
types: ["textStyle"],
};
},
addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
backgroundColor: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute("data-background-color"),
renderHTML: (attributes: { backgroundColor: string }) => {
const { backgroundColor } = attributes;
if (!backgroundColor) {
return {};
}
let elementAttributes: Record<string, string> = {
"data-background-color": backgroundColor,
};
if (!COLORS_LIST.find((c) => c.key === backgroundColor)) {
elementAttributes = {
...elementAttributes,
style: `background-color: ${backgroundColor}`,
};
}
return elementAttributes;
},
},
},
},
];
},
addCommands() {
return {
setBackgroundColor:
(backgroundColor: string) =>
({ chain }) =>
chain().setMark("textStyle", { backgroundColor }).run(),
unsetBackgroundColor:
() =>
({ chain }) =>
chain().setMark("textStyle", { backgroundColor: null }).run(),
};
},
});
@@ -0,0 +1,133 @@
import { Mark, mergeAttributes } from "@tiptap/core";
// constants
import { COLORS_LIST } from "@/constants/common";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
color: {
/**
* Set the text color
* @param {string} color The color to set
* @example editor.commands.setTextColor('red')
*/
setTextColor: (color: string) => ReturnType;
/**
* Unset the text color
* @example editor.commands.unsetTextColor()
*/
unsetTextColor: () => ReturnType;
/**
* Set the background color
* @param {string} backgroundColor The color to set
* @example editor.commands.setBackgroundColor('red')
*/
setBackgroundColor: (backgroundColor: string) => ReturnType;
/**
* Unset the background color
* @example editor.commands.unsetBackgroundColorColor()
*/
unsetBackgroundColor: () => ReturnType;
};
}
}
export const CustomColorExtension = Mark.create({
name: "customColor",
addOptions() {
return {
HTMLAttributes: {},
};
},
addAttributes() {
return {
color: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute("data-text-color"),
renderHTML: (attributes: { color: string }) => {
const { color } = attributes;
if (!color) {
return {};
}
let elementAttributes: Record<string, string> = {
"data-text-color": color,
};
if (!COLORS_LIST.find((c) => c.key === color)) {
elementAttributes = {
...elementAttributes,
style: `color: ${color}`,
};
}
return elementAttributes;
},
},
backgroundColor: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute("data-background-color"),
renderHTML: (attributes: { backgroundColor: string }) => {
const { backgroundColor } = attributes;
if (!backgroundColor) {
return {};
}
let elementAttributes: Record<string, string> = {
"data-background-color": backgroundColor,
};
if (!COLORS_LIST.find((c) => c.key === backgroundColor)) {
elementAttributes = {
...elementAttributes,
style: `background-color: ${backgroundColor}`,
};
}
return elementAttributes;
},
},
};
},
parseHTML() {
return [
{
tag: "span",
getAttrs: (node) => node.getAttribute("data-text-color") && null,
},
{
tag: "span",
getAttrs: (node) => node.getAttribute("data-background-color") && null,
},
];
},
renderHTML({ HTMLAttributes }) {
return ["span", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
addCommands() {
return {
setTextColor:
(color: string) =>
({ chain }) =>
chain().setMark(this.name, { color }).run(),
unsetTextColor:
() =>
({ chain }) =>
chain().setMark(this.name, { color: null }).run(),
setBackgroundColor:
(backgroundColor: string) =>
({ chain }) =>
chain().setMark(this.name, { backgroundColor }).run(),
unsetBackgroundColor:
() =>
({ chain }) =>
chain().setMark(this.name, { backgroundColor: null }).run(),
};
},
});
@@ -59,12 +59,12 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
src: remoteImageSrc,
setEditorContainer,
} = props;
const { width, height, aspectRatio } = node.attrs;
const { width: nodeWidth, height: nodeHeight, aspectRatio: nodeAspectRatio } = node.attrs;
// states
const [size, setSize] = useState<Size>({
width: ensurePixelString(width, "35%"),
height: ensurePixelString(height, "auto"),
aspectRatio: aspectRatio || 1,
width: ensurePixelString(nodeWidth, "35%"),
height: ensurePixelString(nodeHeight, "auto"),
aspectRatio: nodeAspectRatio || null,
});
const [isResizing, setIsResizing] = useState(false);
const [initialResizeComplete, setInitialResizeComplete] = useState(false);
@@ -72,6 +72,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
const containerRef = useRef<HTMLDivElement>(null);
const containerRect = useRef<DOMRect | null>(null);
const imageRef = useRef<HTMLImageElement>(null);
const [hasErroredOnFirstLoad, setHasErroredOnFirstLoad] = useState(false);
const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false);
const updateAttributesSafely = useCallback(
(attributes: Partial<ImageAttributes>, errorMessage: string) => {
@@ -104,17 +106,17 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
}
setEditorContainer(closestEditorContainer);
const aspectRatio = img.naturalWidth / img.naturalHeight;
const aspectRatioCalculated = img.naturalWidth / img.naturalHeight;
if (width === "35%") {
if (nodeWidth === "35%") {
const editorWidth = closestEditorContainer.clientWidth;
const initialWidth = Math.max(editorWidth * 0.35, MIN_SIZE);
const initialHeight = initialWidth / aspectRatio;
const initialHeight = initialWidth / aspectRatioCalculated;
const initialComputedSize = {
width: `${Math.round(initialWidth)}px` satisfies Pixel,
height: `${Math.round(initialHeight)}px` satisfies Pixel,
aspectRatio: aspectRatio,
aspectRatio: aspectRatioCalculated,
};
setSize(initialComputedSize);
@@ -124,9 +126,10 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
);
} else {
// as the aspect ratio in not stored for old images, we need to update the attrs
if (!aspectRatio) {
// or if aspectRatioCalculated from the image's width and height doesn't match stored aspectRatio then also we'll update the attrs
if (!nodeAspectRatio || nodeAspectRatio !== aspectRatioCalculated) {
setSize((prevSize) => {
const newSize = { ...prevSize, aspectRatio };
const newSize = { ...prevSize, aspectRatio: aspectRatioCalculated };
updateAttributesSafely(
newSize,
"Failed to update attributes while initializing images with width but no aspect ratio:"
@@ -136,16 +139,17 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
}
}
setInitialResizeComplete(true);
}, [width, updateAttributes, editorContainer, aspectRatio]);
}, [nodeWidth, updateAttributes, editorContainer, nodeAspectRatio]);
// for real time resizing
useLayoutEffect(() => {
setSize((prevSize) => ({
...prevSize,
width: ensurePixelString(width),
height: ensurePixelString(height),
width: ensurePixelString(nodeWidth),
height: ensurePixelString(nodeHeight),
aspectRatio: nodeAspectRatio,
}));
}, [width, height]);
}, [nodeWidth, nodeHeight, nodeAspectRatio]);
const handleResize = useCallback(
(e: MouseEvent | TouchEvent) => {
@@ -158,7 +162,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` }));
},
[size]
[size.aspectRatio]
);
const handleResizeEnd = useCallback(() => {
@@ -181,11 +185,15 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
window.addEventListener("mousemove", handleResize);
window.addEventListener("mouseup", handleResizeEnd);
window.addEventListener("mouseleave", handleResizeEnd);
window.addEventListener("touchmove", handleResize);
window.addEventListener("touchend", handleResizeEnd);
return () => {
window.removeEventListener("mousemove", handleResize);
window.removeEventListener("mouseup", handleResizeEnd);
window.removeEventListener("mouseleave", handleResizeEnd);
window.removeEventListener("touchmove", handleResize);
window.removeEventListener("touchend", handleResizeEnd);
};
}
}, [isResizing, handleResize, handleResizeEnd]);
@@ -202,7 +210,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete;
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
// show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
const showImageUtils = remoteImageSrc && initialResizeComplete;
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
@@ -217,7 +225,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
onMouseDown={handleImageMouseDown}
style={{
width: size.width,
aspectRatio: size.aspectRatio,
...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
}}
>
{showImageLoader && (
@@ -230,9 +238,26 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
ref={imageRef}
src={displayedImageSrc}
onLoad={handleImageLoad}
onError={(e) => {
console.error("Error loading image", e);
setFailedToLoadImage(true);
onError={async (e) => {
// for old image extension this command doesn't exist or if the image failed to load for the first time
if (!editor?.commands.restoreImage || hasTriedRestoringImageOnce) {
setFailedToLoadImage(true);
return;
}
try {
setHasErroredOnFirstLoad(true);
// this is a type error from tiptap, don't remove await until it's fixed
await editor?.commands.restoreImage?.(node.attrs.src);
imageRef.current.src = remoteImageSrc;
} catch {
// if the image failed to even restore, then show the error state
setFailedToLoadImage(true);
console.error("Error while loading image", e);
} finally {
setHasErroredOnFirstLoad(false);
setHasTriedRestoringImageOnce(true);
}
}}
width={size.width}
className={cn("image-component block rounded-md", {
@@ -243,7 +268,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
})}
style={{
width: size.width,
aspectRatio: size.aspectRatio,
...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
}}
/>
{showImageUtils && (
@@ -283,6 +308,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
}
)}
onMouseDown={handleResizeStart}
onTouchStart={handleResizeStart}
/>
</>
)}
@@ -1,21 +1,23 @@
import { useEffect, useRef, useState } from "react";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { Editor, NodeViewWrapper } from "@tiptap/react";
import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
// extensions
import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image";
export type CustomImageNodeViewProps = {
export type CustomImageComponentProps = {
getPos: () => number;
editor: Editor;
node: ProsemirrorNode & {
node: NodeViewProps["node"] & {
attrs: ImageAttributes;
};
updateAttributes: (attrs: Record<string, any>) => void;
updateAttributes: (attrs: ImageAttributes) => void;
selected: boolean;
};
export type CustomImageNodeViewProps = NodeViewProps & CustomImageComponentProps;
export const CustomImageNode = (props: CustomImageNodeViewProps) => {
const { getPos, editor, node, updateAttributes, selected } = props;
const { src: remoteImageSrc } = node.attrs;
const [isUploaded, setIsUploaded] = useState(false);
const [imageFromFileSystem, setImageFromFileSystem] = useState<string | undefined>(undefined);
@@ -37,14 +39,13 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
// the image is already uploaded if the image-component node has src attribute
// and we need to remove the blob from our file system
useEffect(() => {
const remoteImageSrc = node.attrs.src;
if (remoteImageSrc) {
setIsUploaded(true);
setImageFromFileSystem(undefined);
} else {
setIsUploaded(false);
}
}, [node.attrs.src]);
}, [remoteImageSrc]);
return (
<NodeViewWrapper>
@@ -55,7 +56,7 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
editorContainer={editorContainer}
editor={editor}
// @ts-expect-error function not expected here, but will still work
src={editor?.commands?.getImageSource?.(node.attrs.src)}
src={editor?.commands?.getImageSource?.(remoteImageSrc)}
getPos={getPos}
node={node}
setEditorContainer={setEditorContainer}
@@ -1,27 +1,20 @@
import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { Editor } from "@tiptap/core";
import { ImageIcon } from "lucide-react";
// helpers
import { cn } from "@/helpers/common";
// hooks
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
// extensions
import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image";
import { type CustomImageComponentProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
export const CustomImageUploader = (props: {
editor: Editor;
failedToLoadImage: boolean;
getPos: () => number;
loadImageFromFileSystem: (file: string) => void;
type CustomImageUploaderProps = CustomImageComponentProps & {
maxFileSize: number;
node: ProsemirrorNode & {
attrs: ImageAttributes;
};
selected: boolean;
loadImageFromFileSystem: (file: string) => void;
failedToLoadImage: boolean;
setIsUploaded: (isUploaded: boolean) => void;
updateAttributes: (attrs: Record<string, any>) => void;
}) => {
};
export const CustomImageUploader = (props: CustomImageUploaderProps) => {
const {
editor,
failedToLoadImage,
@@ -36,8 +29,8 @@ export const CustomImageUploader = (props: {
// refs
const fileInputRef = useRef<HTMLInputElement>(null);
const hasTriggeredFilePickerRef = useRef(false);
const { id: imageEntityId } = node.attrs;
// derived values
const imageEntityId = node.attrs.id;
const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]);
const onUpload = useCallback(
@@ -1,9 +1,11 @@
import { Editor, mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
import { Node } from "@tiptap/pm/model";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
// extensions
import { CustomImageNode } from "@/extensions/custom-image";
import { CustomImageNode, ImageAttributes } from "@/extensions/custom-image";
// plugins
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
// types
@@ -22,6 +24,7 @@ declare module "@tiptap/core" {
imageComponent: {
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
uploadImage: (file: File) => () => Promise<string> | undefined;
restoreImage: (src: string) => () => Promise<void>;
getImageSource?: (path: string) => () => string;
};
}
@@ -40,8 +43,8 @@ export const CustomImageExtension = (props: TFileHandler) => {
const {
getAssetSrc,
upload,
delete: deleteImage,
restore: restoreImage,
delete: deleteImageFn,
restore: restoreImageFn,
validation: { maxFileSize },
} = props;
@@ -85,22 +88,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
return ["image-component", mergeAttributes(HTMLAttributes)];
},
onCreate(this) {
const imageSources = new Set<string>();
this.editor.state.doc.descendants((node) => {
if (node.type.name === this.name) {
imageSources.add(node.attrs.src);
}
});
imageSources.forEach(async (src) => {
try {
await restoreImage(src);
} catch (error) {
console.error("Error restoring image: ", error);
}
});
},
addKeyboardShortcuts() {
return {
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
@@ -110,17 +97,44 @@ export const CustomImageExtension = (props: TFileHandler) => {
addProseMirrorPlugins() {
return [
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
];
},
onCreate(this) {
const imageSources = new Set<string>();
this.editor.state.doc.descendants((node) => {
if (node.type.name === this.name) {
if (!node.attrs.src?.startsWith("http")) return;
imageSources.add(node.attrs.src);
}
});
imageSources.forEach(async (src) => {
try {
await restoreImageFn(src);
} catch (error) {
console.error("Error restoring image: ", error);
}
});
},
addStorage() {
return {
fileMap: new Map(),
deletedImageSet: new Map<string, boolean>(),
uploadInProgress: false,
maxFileSize,
markdown: {
serialize(state: MarkdownSerializerState, node: Node) {
const attrs = node.attrs as ImageAttributes;
const imageSource = state.esc(this?.editor?.commands?.getImageSource?.(attrs.src) || attrs.src);
const imageWidth = state.esc(attrs.width?.toString());
state.write(`<img src="${state.esc(imageSource)}" width="${imageWidth}" />`);
state.closeBlock(node);
},
},
};
},
@@ -179,6 +193,9 @@ export const CustomImageExtension = (props: TFileHandler) => {
const fileUrl = await upload(file);
return fileUrl;
},
restoreImage: (src: string) => async () => {
await restoreImageFn(src);
},
getImageSource: (path: string) => () => getAssetSrc(path),
};
},
@@ -1,8 +1,10 @@
import { mergeAttributes } from "@tiptap/core";
import { Image } from "@tiptap/extension-image";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
import { Node } from "@tiptap/pm/model";
import { ReactNodeViewRenderer } from "@tiptap/react";
// components
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
import { CustomImageNode, ImageAttributes, UploadImageExtensionStorage } from "@/extensions/custom-image";
// types
import { TFileHandler } from "@/types";
@@ -52,6 +54,15 @@ export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAsset
addStorage() {
return {
fileMap: new Map(),
markdown: {
serialize(state: MarkdownSerializerState, node: Node) {
const attrs = node.attrs as ImageAttributes;
const imageSource = state.esc(this?.editor?.commands?.getImageSource?.(attrs.src) || attrs.src);
const imageWidth = state.esc(attrs.width?.toString());
state.write(`<img src="${state.esc(imageSource)}" width="${imageWidth}" />`);
state.closeBlock(node);
},
},
};
},
@@ -1,78 +0,0 @@
import { Extension } from "@tiptap/core";
// constants
import { COLORS_LIST } from "@/constants/common";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
color: {
/**
* Set the text color
* @param color The color to set
* @example editor.commands.setColor('red')
*/
setTextColor: (color: string) => ReturnType;
/**
* Unset the text color
* @example editor.commands.unsetColor()
*/
unsetTextColor: () => ReturnType;
};
}
}
export const CustomTextColorExtension = Extension.create({
name: "customTextColor",
addOptions() {
return {
types: ["textStyle"],
};
},
addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
color: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute("data-text-color"),
renderHTML: (attributes: { color: string }) => {
const { color } = attributes;
if (!color) {
return {};
}
let elementAttributes: Record<string, string> = {
"data-text-color": color,
};
if (!COLORS_LIST.find((c) => c.key === color)) {
elementAttributes = {
...elementAttributes,
style: `color: ${color}`,
};
}
return elementAttributes;
},
},
},
},
];
},
addCommands() {
return {
setTextColor:
(color: string) =>
({ chain }) =>
chain().setMark("textStyle", { color }).run(),
unsetTextColor:
() =>
({ chain }) =>
chain().setMark("textStyle", { color: null }).run(),
};
},
});
@@ -8,17 +8,16 @@ import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
// extensions
import {
CustomBackgroundColorExtension,
CustomCodeBlockExtension,
CustomCodeInlineExtension,
CustomCodeMarkPlugin,
CustomColorExtension,
CustomHorizontalRule,
CustomImageExtension,
CustomKeymap,
CustomLinkExtension,
CustomMention,
CustomQuoteExtension,
CustomTextColorExtension,
CustomTypographyExtension,
DropHandlerExtension,
ImageExtension,
@@ -141,7 +140,10 @@ export const CoreEditorExtensions = (args: TArguments) => {
if (editor.storage.imageComponent.uploadInProgress) return "";
const shouldHidePlaceholder =
editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");
editor.isActive("table") ||
editor.isActive("codeBlock") ||
editor.isActive("image") ||
editor.isActive("imageComponent");
if (shouldHidePlaceholder) return "";
@@ -155,7 +157,6 @@ export const CoreEditorExtensions = (args: TArguments) => {
includeChildren: true,
}),
CharacterCount,
CustomTextColorExtension,
CustomBackgroundColorExtension,
CustomColorExtension,
];
};
@@ -11,9 +11,9 @@ import { CustomImageNode } from "@/extensions";
export const ImageExtension = (fileHandler: TFileHandler) => {
const {
delete: deleteImage,
getAssetSrc,
restore: restoreImage,
delete: deleteImageFn,
restore: restoreImageFn,
validation: { maxFileSize },
} = fileHandler;
@@ -24,10 +24,11 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
};
},
addProseMirrorPlugins() {
return [
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
];
},
@@ -35,12 +36,14 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
const imageSources = new Set<string>();
this.editor.state.doc.descendants((node) => {
if (node.type.name === this.name) {
if (!node.attrs.src?.startsWith("http")) return;
imageSources.add(node.attrs.src);
}
});
imageSources.forEach(async (src) => {
try {
await restoreImage(src);
await restoreImageFn(src);
} catch (error) {
console.error("Error restoring image: ", error);
}
@@ -65,6 +68,9 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
height: {
default: null,
},
aspectRatio: {
default: null,
},
};
},
@@ -11,6 +11,9 @@ export const ImageExtensionWithoutProps = () =>
height: {
default: null,
},
aspectRatio: {
default: null,
},
};
},
});
@@ -18,6 +18,9 @@ export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">)
height: {
default: null,
},
aspectRatio: {
default: null,
},
};
},
+1 -2
View File
@@ -10,9 +10,8 @@ export * from "./slash-commands";
export * from "./table";
export * from "./typography";
export * from "./core-without-props";
export * from "./custom-background-color";
export * from "./custom-code-inline";
export * from "./custom-text-color";
export * from "./custom-color";
export * from "./drop";
export * from "./enter-key-extension";
export * from "./extensions";
@@ -1,5 +1,7 @@
import { Editor, mergeAttributes } from "@tiptap/core";
import Mention, { MentionOptions } from "@tiptap/extension-mention";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
import { Node } from "@tiptap/pm/model";
import { ReactNodeViewRenderer, ReactRenderer } from "@tiptap/react";
import tippy from "tippy.js";
// extensions
@@ -25,8 +27,19 @@ export const CustomMention = ({
addStorage(this) {
return {
mentionsOpen: false,
markdown: {
serialize(state: MarkdownSerializerState, node: Node) {
const { attrs } = node;
const label = `@${state.esc(attrs.label)}`;
const originUrl = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
const safeRedirectionPath = state.esc(attrs.redirect_uri);
const url = `${originUrl}${safeRedirectionPath}`;
state.write(`[${label}](${url})`);
},
},
};
},
addAttributes() {
return {
id: {
@@ -21,8 +21,7 @@ import {
CustomMention,
HeadingListExtension,
CustomReadOnlyImageExtension,
CustomTextColorExtension,
CustomBackgroundColorExtension,
CustomColorExtension,
} from "@/extensions";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
@@ -123,8 +122,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
readonly: true,
}),
CharacterCount,
CustomTextColorExtension,
CustomBackgroundColorExtension,
CustomColorExtension,
HeadingListExtension,
];
};
@@ -25,6 +25,9 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor
if (node.type.name !== nodeType) return;
if (pos < 0 || pos > newState.doc.content.size) return;
if (oldImageSources.has(node.attrs.src)) return;
// if the src is just a id (private bucket), then we don't need to handle restore from here but
// only while it fails to load
if (!node.attrs.src?.startsWith("http")) return;
addedImages.push(node as ImageNode);
});
+2 -2
View File
@@ -14,8 +14,8 @@
},
"devDependencies": {
"@types/node": "^22.5.4",
"@types/react": "^18.3.5",
"typescript": "^5.6.2",
"@types/react": "^18.3.11",
"typescript": "^5.3.3",
"tsup": "^7.2.0"
},
"dependencies": {
+13
View File
@@ -96,3 +96,16 @@ export type TInboxIssuePaginationInfo = TPaginationInfo & {
export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & {
results: TInboxIssue[];
};
export type TInboxForm = {
anchor: string;
id: string;
is_disabled: boolean;
};
export type TInboxIssueForm = {
name: string;
description: string;
username: string;
email: string;
};
+1
View File
@@ -54,6 +54,7 @@ export interface IProject {
updated_by: string;
workspace: IWorkspace | string;
workspace_detail: IWorkspaceLite;
timezone: string;
}
export interface IProjectLite {
+1
View File
@@ -91,6 +91,7 @@ export interface IWorkspaceMemberMe {
updated_by: string;
view_props: IWorkspaceViewProps;
workspace: string;
draft_issue_count: number;
}
export interface ILastActiveWorkspaceDetails {
+6 -4
View File
@@ -20,6 +20,10 @@
"postcss": "postcss styles/globals.css -o styles/output.css --watch",
"lint": "eslint src --ext .ts,.tsx"
},
"peerDependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
@@ -33,7 +37,6 @@
"lodash": "^4.17.21",
"lucide-react": "^0.379.0",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-popper": "^2.3.0",
"sonner": "^1.4.41",
"tailwind-merge": "^2.0.0",
@@ -53,15 +56,14 @@
"@storybook/test": "^8.1.1",
"@types/lodash": "^4.17.6",
"@types/node": "^20.5.2",
"@types/react": "^18.2.42",
"@types/react": "^18.3.11",
"@types/react-color": "^3.0.9",
"@types/react-dom": "^18.2.17",
"@types/react-dom": "^18.2.18",
"autoprefixer": "^10.4.19",
"classnames": "^2.3.2",
"@plane/eslint-config": "*",
"postcss-cli": "^11.0.0",
"postcss-nested": "^6.0.1",
"react": "^18.2.0",
"storybook": "^8.1.1",
"tailwind-config-custom": "*",
"tailwindcss": "^3.4.3",
+1
View File
@@ -31,3 +31,4 @@ export * from "./favorite-folder-icon";
export * from "./planned-icon";
export * from "./in-progress-icon";
export * from "./done-icon";
export * from "./pending-icon";
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const PendingState: React.FC<ISvgIcons> = ({ width = "10", height = "11", className, color = "#455068" }) => (
<svg
width={width}
height={height}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12Z"
fill={color}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 5C12.5523 5 13 5.44772 13 6V11.382L16.4472 13.1056C16.9412 13.3526 17.1414 13.9532 16.8944 14.4472C16.6474 14.9412 16.0468 15.1414 15.5528 14.8944L11.5528 12.8944C11.214 12.725 11 12.3788 11 12V6C11 5.44772 11.4477 5 12 5Z"
fill={color}
/>
</svg>
);
+1 -3
View File
@@ -16,7 +16,7 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
const { priority, className = "", containerClassName = "", size = 14, withContainer = false } = props;
const priorityClasses = {
urgent: "bg-red-600 text-red-500 border-red-600",
urgent: "bg-red-600/20 text-red-600 border-red-600",
high: "bg-orange-500/20 text-orange-500 border-orange-500",
medium: "bg-yellow-500/20 text-yellow-500 border-yellow-500",
low: "bg-custom-primary-100/20 text-custom-primary-100 border-custom-primary-100",
@@ -49,8 +49,6 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
size={size}
className={cn(
{
"text-white": priority === "urgent",
// centre align the icons
"translate-x-[0.0625rem]": priority === "high",
"translate-x-0.5": priority === "medium",
"translate-x-1": priority === "low",
+2 -1
View File
@@ -18,6 +18,7 @@ export const Popover = (props: TPopover) => {
panelClassName = "",
children,
popoverButtonRef,
buttonRefClassName = "",
} = props;
// states
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
@@ -38,7 +39,7 @@ export const Popover = (props: TPopover) => {
return (
<HeadlessReactPopover className={cn("relative flex h-full w-full items-center justify-center", popoverClassName)}>
<div ref={setReferenceElement} className="w-full">
<div ref={setReferenceElement} className={cn("w-full", buttonRefClassName)}>
<HeadlessReactPopover.Button
ref={popoverButtonRef as Ref<HTMLButtonElement>}
className={cn(
+1
View File
@@ -5,6 +5,7 @@ export type TPopoverButtonDefaultOptions = {
// button and button styling
button?: ReactNode;
buttonClassName?: string;
buttonRefClassName?: string;
disabled?: boolean;
};
@@ -16,6 +16,7 @@ export class FileUploadService extends APIService {
"Content-Type": "multipart/form-data",
},
cancelToken: this.cancelSource.token,
withCredentials: false,
})
.then((response) => response?.data)
.catch((error) => {
+4 -5
View File
@@ -4,7 +4,6 @@ import { TFileHandler } from "@plane/editor";
import { MAX_FILE_SIZE } from "@/constants/common";
// helpers
import { getFileURL } from "@/helpers/file.helper";
import { checkURLValidity } from "@/helpers/string.helper";
// services
import { FileService } from "@/services/file.service";
const fileService = new FileService();
@@ -34,7 +33,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
return {
getAssetSrc: (path) => {
if (!path) return "";
if (checkURLValidity(path)) {
if (path?.startsWith("http")) {
return path;
} else {
return getEditorAssetSrc(anchor, path) ?? "";
@@ -42,14 +41,14 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
},
upload: uploadFile,
delete: async (src: string) => {
if (checkURLValidity(src)) {
if (src?.startsWith("http")) {
await fileService.deleteOldEditorAsset(workspaceId, src);
} else {
await fileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? "");
}
},
restore: async (src: string) => {
if (checkURLValidity(src)) {
if (src?.startsWith("http")) {
await fileService.restoreOldEditorAsset(workspaceId, src);
} else {
await fileService.restoreNewAsset(anchor, src);
@@ -73,7 +72,7 @@ export const getReadOnlyEditorFileHandlers = (
return {
getAssetSrc: (path) => {
if (!path) return "";
if (checkURLValidity(path)) {
if (path?.startsWith("http")) {
return path;
} else {
return getEditorAssetSrc(anchor, path) ?? "";
+2 -2
View File
@@ -54,8 +54,8 @@
"@types/lodash": "^4.17.1",
"@types/node": "18.14.1",
"@types/nprogress": "^0.2.0",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.1",
"@types/zxcvbn": "^4.4.4",
"@typescript-eslint/eslint-plugin": "^5.48.2",
Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

@@ -16,7 +16,7 @@ export const WorkspaceActiveCycleHeader = observer(() => (
type="text"
link={
<BreadcrumbLink
label="Active Cycles"
label="Active cycles"
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300 rotate-180" />}
/>
}
@@ -1,6 +1,6 @@
"use client";
import { FC, useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { PenSquare } from "lucide-react";
// ui
@@ -11,16 +11,17 @@ import { CreateUpdateIssueModal } from "@/components/issues";
// constants
import { EIssuesStoreType } from "@/constants/issue";
// hooks
import { useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store";
import { useProject, useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store";
// plane-web
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const WorkspaceDraftHeader: FC = observer(() => {
export const WorkspaceDraftHeader = observer(() => {
// state
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
// store hooks
const { allowPermissions } = useUserPermissions();
const { paginationInfo } = useWorkspaceDraftIssues();
const { joinedProjectIds } = useProject();
// check if user is authorized to create draft issue
const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@@ -41,7 +42,7 @@ export const WorkspaceDraftHeader: FC = observer(() => {
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label={`Draft`} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />}
link={<BreadcrumbLink label={`Drafts`} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
{paginationInfo?.total_count && paginationInfo?.total_count > 0 ? (
@@ -53,15 +54,17 @@ export const WorkspaceDraftHeader: FC = observer(() => {
</Header.LeftItem>
<Header.RightItem>
<Button
variant="primary"
size="sm"
className="items-center gap-1"
onClick={() => setIsDraftIssueModalOpen(true)}
disabled={!isAuthorizedUser}
>
Draft <span className="hidden sm:inline-block">issue</span>
</Button>
{joinedProjectIds && joinedProjectIds.length > 0 && (
<Button
variant="primary"
size="sm"
className="items-center gap-1"
onClick={() => setIsDraftIssueModalOpen(true)}
disabled={!isAuthorizedUser}
>
Draft<span className="hidden sm:inline-block"> an issue</span>
</Button>
)}
</Header.RightItem>
</Header>
</>
@@ -2,7 +2,7 @@
// components
import { AppHeader, ContentWrapper } from "@/components/core";
import { ProjectInboxHeader } from "./header";
import { ProjectInboxHeader } from "@/plane-web/components/projects/settings/intake";
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
return (

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