From 32e2362d6461696f2ae76a84be8685baf9093973 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Sun, 13 Oct 2024 00:18:46 +0530 Subject: [PATCH 1/3] fix: workspace level issue creation --- web/core/components/issues/issue-modal/base.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index cf86fb516e7..73ccd8b9ac1 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -16,7 +16,6 @@ import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useEventTracker, useCycle, useIssues, useModule, useIssueDetail, useUser } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; -import useLocalStorage from "@/hooks/use-local-storage"; // services import { FileService } from "@/services/file.service"; const fileService = new FileService(); @@ -168,7 +167,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( if (uploadedAssetIds.length > 0) { await fileService.updateBulkProjectAssetsUploadStatus( workspaceSlug?.toString() ?? "", - projectId, + activeProjectId ?? "", response?.id ?? "", { asset_ids: uploadedAssetIds, From adbf22c7baf7ecba18208148ee41da542ebb6337 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sun, 13 Oct 2024 00:19:15 +0530 Subject: [PATCH 2/3] dev: add draft issue support, fix your work tab and cache invalidation for workspace level logos --- apiserver/plane/app/views/asset/v2.py | 150 ++++++++++++++++-- apiserver/plane/app/views/workspace/user.py | 2 +- ...draft_issue_alter_fileasset_entity_type.py | 45 ++++++ apiserver/plane/db/models/asset.py | 9 ++ 4 files changed, 190 insertions(+), 16 deletions(-) create mode 100644 apiserver/plane/db/migrations/0080_fileasset_draft_issue_alter_fileasset_entity_type.py diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index dfb5a233116..fba1073f8be 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -21,6 +21,7 @@ ) from plane.settings.storage import S3Storage from plane.app.permissions import allow_permission, ROLE +from plane.utils.cache import invalidate_cache_directly class UserAssetsV2Endpoint(BaseAPIView): @@ -35,7 +36,7 @@ def asset_delete(self, asset_id): asset.save() return - def entity_asset_save(self, asset_id, entity_type, asset): + def entity_asset_save(self, asset_id, entity_type, asset, request): # User Avatar if entity_type == FileAsset.EntityTypeContext.USER_AVATAR: user = User.objects.get(id=asset.user_id) @@ -46,6 +47,18 @@ def entity_asset_save(self, asset_id, entity_type, asset): # Save the new avatar user.avatar_asset_id = asset_id user.save() + invalidate_cache_directly( + path="/api/users/me/", + url_params=False, + user=True, + request=request, + ) + invalidate_cache_directly( + path="/api/users/me/settings/", + url_params=False, + user=True, + request=request, + ) return # User Cover if entity_type == FileAsset.EntityTypeContext.USER_COVER: @@ -57,21 +70,57 @@ def entity_asset_save(self, asset_id, entity_type, asset): # Save the new cover image user.cover_image_asset_id = asset_id user.save() + invalidate_cache_directly( + path="/api/users/me/", + url_params=False, + user=True, + request=request, + ) + invalidate_cache_directly( + path="/api/users/me/settings/", + url_params=False, + user=True, + request=request, + ) return return - def entity_asset_delete(self, entity_type, asset): + def entity_asset_delete(self, entity_type, asset, request): # User Avatar if entity_type == FileAsset.EntityTypeContext.USER_AVATAR: user = User.objects.get(id=asset.user_id) user.avatar_asset_id = None user.save() + invalidate_cache_directly( + path="/api/users/me/", + url_params=False, + user=True, + request=request, + ) + invalidate_cache_directly( + path="/api/users/me/settings/", + url_params=False, + user=True, + request=request, + ) return # User Cover if entity_type == FileAsset.EntityTypeContext.USER_COVER: user = User.objects.get(id=asset.user_id) user.cover_image_asset_id = None user.save() + invalidate_cache_directly( + path="/api/users/me/", + url_params=False, + user=True, + request=request, + ) + invalidate_cache_directly( + path="/api/users/me/settings/", + url_params=False, + user=True, + request=request, + ) return return @@ -82,6 +131,9 @@ def post(self, request): size = int(request.data.get("size", settings.FILE_SIZE_LIMIT)) entity_type = request.data.get("entity_type", False) + # Check if the file size is within the limit + size_limit = min(size, settings.FILE_SIZE_LIMIT) + # Check if the entity type is allowed if not entity_type or entity_type not in ["USER_AVATAR", "USER_COVER"]: return Response( @@ -103,9 +155,6 @@ def post(self, request): status=status.HTTP_400_BAD_REQUEST, ) - # Get the size limit - size_limit = min(settings.FILE_SIZE_LIMIT, size) - # asset key asset_key = f"{uuid.uuid4().hex}-{name}" @@ -153,7 +202,12 @@ def patch(self, request, asset_id): object_name=asset.asset.name ) # get the entity and save the asset id for the request field - self.entity_asset_save(asset_id, asset.entity_type, asset) + self.entity_asset_save( + asset_id=asset_id, + entity_type=asset.entity_type, + asset=asset, + request=request, + ) # update the attributes asset.attributes = request.data.get("attributes", asset.attributes) # save the asset @@ -165,7 +219,9 @@ def delete(self, request, asset_id): asset.is_deleted = True asset.deleted_at = timezone.now() # get the entity and save the asset id for the request field - self.entity_asset_delete(asset.entity_type, asset) + self.entity_asset_delete( + entity_type=asset.entity_type, asset=asset, request=request + ) asset.save() return Response(status=status.HTTP_204_NO_CONTENT) @@ -174,16 +230,19 @@ class WorkspaceFileAssetEndpoint(BaseAPIView): """This endpoint is used to upload cover images/logos etc for workspace, projects and users.""" def get_entity_id_field(self, entity_type, entity_id): + # Workspace Logo if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO: return { "workspace_id": entity_id, } + # Project Cover if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER: return { "project_id": entity_id, } + # User Avatar and Cover if entity_type in [ FileAsset.EntityTypeContext.USER_AVATAR, FileAsset.EntityTypeContext.USER_COVER, @@ -192,6 +251,7 @@ def get_entity_id_field(self, entity_type, entity_id): "user_id": entity_id, } + # Issue Attachment and Description if entity_type in [ FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, FileAsset.EntityTypeContext.ISSUE_DESCRIPTION, @@ -200,11 +260,13 @@ def get_entity_id_field(self, entity_type, entity_id): "issue_id": entity_id, } + # Page Description if entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION: return { "page_id": entity_id, } + # Comment Description if entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION: return { "comment_id": entity_id, @@ -222,7 +284,7 @@ def asset_delete(self, asset_id): asset.save() return - def entity_asset_save(self, asset_id, entity_type, asset): + def entity_asset_save(self, asset_id, entity_type, asset, request): # Workspace Logo if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO: workspace = Workspace.objects.filter(id=asset.workspace_id).first() @@ -235,6 +297,24 @@ def entity_asset_save(self, asset_id, entity_type, asset): workspace.logo = "" workspace.logo_asset_id = asset_id workspace.save() + invalidate_cache_directly( + path="/api/workspaces/", + url_params=False, + user=False, + request=request, + ) + invalidate_cache_directly( + path="/api/users/me/workspaces/", + url_params=False, + user=True, + request=request, + ) + invalidate_cache_directly( + path="/api/instances/", + url_params=False, + user=False, + request=request, + ) return # Project Cover @@ -253,7 +333,7 @@ def entity_asset_save(self, asset_id, entity_type, asset): else: return - def entity_asset_delete(self, entity_type, asset): + def entity_asset_delete(self, entity_type, asset, request): # Workspace Logo if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO: workspace = Workspace.objects.get(id=asset.workspace_id) @@ -261,6 +341,24 @@ def entity_asset_delete(self, entity_type, asset): return workspace.logo_asset_id = None workspace.save() + invalidate_cache_directly( + path="/api/workspaces/", + url_params=False, + user=False, + request=request, + ) + invalidate_cache_directly( + path="/api/users/me/workspaces/", + url_params=False, + user=True, + request=request, + ) + invalidate_cache_directly( + path="/api/instances/", + url_params=False, + user=False, + request=request, + ) return # Project Cover elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER: @@ -322,7 +420,9 @@ def post(self, request, slug): workspace=workspace, created_by=request.user, entity_type=entity_type, - **self.get_entity_id_field(entity_type, entity_identifier), + **self.get_entity_id_field( + entity_type=entity_type, entity_id=entity_identifier + ), ) # Get the presigned URL @@ -355,7 +455,12 @@ def patch(self, request, slug, asset_id): object_name=asset.asset.name ) # get the entity and save the asset id for the request field - self.entity_asset_save(asset_id, asset.entity_type, asset) + self.entity_asset_save( + asset_id=asset_id, + entity_type=asset.entity_type, + asset=asset, + request=request, + ) # update the attributes asset.attributes = request.data.get("attributes", asset.attributes) # save the asset @@ -367,7 +472,9 @@ def delete(self, request, slug, asset_id): asset.is_deleted = True asset.deleted_at = timezone.now() # get the entity and save the asset id for the request field - self.entity_asset_delete(asset.entity_type, asset) + self.entity_asset_delete( + entity_type=asset.entity_type, asset=asset, request=request + ) asset.save() return Response(status=status.HTTP_204_NO_CONTENT) @@ -454,7 +561,7 @@ def post(self, request, slug, asset_id): class ProjectAssetEndpoint(BaseAPIView): """This endpoint is used to upload cover images/logos etc for workspace, projects and users.""" - def get_entity_id_fiekd(self, entity_type, entity_id): + def get_entity_id_field(self, entity_type, entity_id): if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO: return { "workspace_id": entity_id, @@ -490,6 +597,11 @@ def get_entity_id_fiekd(self, entity_type, entity_id): return { "comment_id": entity_id, } + + if entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION: + return { + "draft_issue_id": entity_id, + } return {} @allow_permission( @@ -513,7 +625,7 @@ def post(self, request, slug, project_id): ) # Check if the file type is allowed - allowed_types = ["image/jpeg", "image/png", "image/webp"] + allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"] if type not in allowed_types: return Response( { @@ -545,7 +657,7 @@ def post(self, request, slug, project_id): created_by=request.user, entity_type=entity_type, project_id=project_id, - **self.get_entity_id_fiekd(entity_type, entity_identifier), + **self.get_entity_id_field(entity_type, entity_identifier), ) # Get the presigned URL @@ -688,4 +800,12 @@ def post(self, request, slug, project_id, entity_id): page_id=entity_id, ) + if ( + asset.entity_type + == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION + ): + assets.update( + draft_issue_id=entity_id, + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace/user.py b/apiserver/plane/app/views/workspace/user.py index fa0bec019bd..57cde8d8bc2 100644 --- a/apiserver/plane/app/views/workspace/user.py +++ b/apiserver/plane/app/views/workspace/user.py @@ -129,7 +129,7 @@ def get(self, request, slug, user_id): ) .annotate( attachment_count=FileAsset.objects.filter( - entity_identifier=OuterRef("id"), + issue_id=OuterRef("id"), entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, ) .order_by() diff --git a/apiserver/plane/db/migrations/0080_fileasset_draft_issue_alter_fileasset_entity_type.py b/apiserver/plane/db/migrations/0080_fileasset_draft_issue_alter_fileasset_entity_type.py new file mode 100644 index 00000000000..f511301930a --- /dev/null +++ b/apiserver/plane/db/migrations/0080_fileasset_draft_issue_alter_fileasset_entity_type.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.15 on 2024-10-12 18:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0079_auto_20241009_0619"), + ] + + operations = [ + migrations.AddField( + model_name="fileasset", + name="draft_issue", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="assets", + to="db.draftissue", + ), + ), + migrations.AlterField( + model_name="fileasset", + name="entity_type", + field=models.CharField( + blank=True, + choices=[ + ("ISSUE_ATTACHMENT", "Issue Attachment"), + ("ISSUE_DESCRIPTION", "Issue Description"), + ("COMMENT_DESCRIPTION", "Comment Description"), + ("PAGE_DESCRIPTION", "Page Description"), + ("USER_COVER", "User Cover"), + ("USER_AVATAR", "User Avatar"), + ("WORKSPACE_LOGO", "Workspace Logo"), + ("PROJECT_COVER", "Project Cover"), + ("DRAFT_ISSUE_ATTACHMENT", "Draft Issue Attachment"), + ("DRAFT_ISSUE_DESCRIPTION", "Draft Issue Description"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index fb6662e7879..e230d3aecf6 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -36,6 +36,8 @@ class EntityTypeContext(models.TextChoices): USER_AVATAR = "USER_AVATAR" WORKSPACE_LOGO = "WORKSPACE_LOGO" PROJECT_COVER = "PROJECT_COVER" + DRAFT_ISSUE_ATTACHMENT = "DRAFT_ISSUE_ATTACHMENT" + DRAFT_ISSUE_DESCRIPTION = "DRAFT_ISSUE_DESCRIPTION" attributes = models.JSONField(default=dict) asset = models.FileField( @@ -54,6 +56,12 @@ class EntityTypeContext(models.TextChoices): null=True, related_name="assets", ) + draft_issue = models.ForeignKey( + "db.DraftIssue", + on_delete=models.CASCADE, + null=True, + related_name="assets", + ) project = models.ForeignKey( "db.Project", on_delete=models.CASCADE, @@ -118,6 +126,7 @@ def asset_url(self): self.EntityTypeContext.ISSUE_DESCRIPTION, self.EntityTypeContext.COMMENT_DESCRIPTION, self.EntityTypeContext.PAGE_DESCRIPTION, + self.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION, ]: return f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/{self.project_id}/{self.id}/" From d22bf9659970a92e39214ef98e7cbdd7a66a0a5a Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sun, 13 Oct 2024 00:29:58 +0530 Subject: [PATCH 3/3] chore: issue description --- packages/types/src/enums.ts | 3 ++- .../issues/issue-modal/components/description-editor.tsx | 6 +++++- web/core/components/issues/issue-modal/form.tsx | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index da6d07f18ac..df6a462b02e 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -53,9 +53,10 @@ export enum EFileAssetType { COMMENT_DESCRIPTION = "COMMENT_DESCRIPTION", ISSUE_ATTACHMENT = "ISSUE_ATTACHMENT", ISSUE_DESCRIPTION = "ISSUE_DESCRIPTION", + DRAFT_ISSUE_DESCRIPTION = "DRAFT_ISSUE_DESCRIPTION", PAGE_DESCRIPTION = "PAGE_DESCRIPTION", PROJECT_COVER = "PROJECT_COVER", USER_AVATAR = "USER_AVATAR", USER_COVER = "USER_COVER", WORKSPACE_LOGO = "WORKSPACE_LOGO", -} \ No newline at end of file +} diff --git a/web/core/components/issues/issue-modal/components/description-editor.tsx b/web/core/components/issues/issue-modal/components/description-editor.tsx index f391b066f87..316668d0107 100644 --- a/web/core/components/issues/issue-modal/components/description-editor.tsx +++ b/web/core/components/issues/issue-modal/components/description-editor.tsx @@ -29,6 +29,7 @@ import { FileService } from "@/services/file.service"; type TIssueDescriptionEditorProps = { control: Control; + isDraft: boolean; issueName: string; issueId: string | undefined; descriptionHtmlData: string | undefined; @@ -52,6 +53,7 @@ const fileService = new FileService(); export const IssueDescriptionEditor: React.FC = observer((props) => { const { control, + isDraft, issueName, issueId, descriptionHtmlData, @@ -194,7 +196,9 @@ export const IssueDescriptionEditor: React.FC = ob projectId, { entity_identifier: issueId ?? "", - entity_type: EFileAssetType.ISSUE_DESCRIPTION, + entity_type: isDraft + ? EFileAssetType.DRAFT_ISSUE_DESCRIPTION + : EFileAssetType.ISSUE_DESCRIPTION, }, file ); diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx index 08eeb9d4296..0cdd347917d 100644 --- a/web/core/components/issues/issue-modal/form.tsx +++ b/web/core/components/issues/issue-modal/form.tsx @@ -320,6 +320,7 @@ export const IssueFormRoot: FC = observer((props) => {