fix: private bucket (#5812)
* fix: workspace level issue creation * dev: add draft issue support, fix your work tab and cache invalidation for workspace level logos * chore: issue description --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
e404450e1a
commit
0ac406e8c7
8 changed files with 199 additions and 20 deletions
|
|
@ -21,6 +21,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
|
||||
|
||||
|
||||
class UserAssetsV2Endpoint(BaseAPIView):
|
||||
|
|
@ -35,7 +36,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
|||
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 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
|||
# 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 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
|||
# 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 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
|||
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 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
|||
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 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
|||
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 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
|||
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 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
"user_id": entity_id,
|
||||
}
|
||||
|
||||
# Issue Attachment and Description
|
||||
if entity_type in [
|
||||
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
|
||||
|
|
@ -200,11 +260,13 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
"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 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
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 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
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 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
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 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
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 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
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 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
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 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
|||
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 @@ class AssetRestoreEndpoint(BaseAPIView):
|
|||
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 @@ class ProjectAssetEndpoint(BaseAPIView):
|
|||
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 @@ class ProjectAssetEndpoint(BaseAPIView):
|
|||
)
|
||||
|
||||
# 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 @@ class ProjectAssetEndpoint(BaseAPIView):
|
|||
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 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
entity_identifier=OuterRef("id"),
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
)
|
||||
.order_by()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -36,6 +36,8 @@ class FileAsset(BaseModel):
|
|||
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 FileAsset(BaseModel):
|
|||
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 @@ class FileAsset(BaseModel):
|
|||
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}/"
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IssuesModalProps> = observer((
|
|||
if (uploadedAssetIds.length > 0) {
|
||||
await fileService.updateBulkProjectAssetsUploadStatus(
|
||||
workspaceSlug?.toString() ?? "",
|
||||
projectId,
|
||||
activeProjectId ?? "",
|
||||
response?.id ?? "",
|
||||
{
|
||||
asset_ids: uploadedAssetIds,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { FileService } from "@/services/file.service";
|
|||
|
||||
type TIssueDescriptionEditorProps = {
|
||||
control: Control<TIssue>;
|
||||
isDraft: boolean;
|
||||
issueName: string;
|
||||
issueId: string | undefined;
|
||||
descriptionHtmlData: string | undefined;
|
||||
|
|
@ -52,6 +53,7 @@ const fileService = new FileService();
|
|||
export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = observer((props) => {
|
||||
const {
|
||||
control,
|
||||
isDraft,
|
||||
issueName,
|
||||
issueId,
|
||||
descriptionHtmlData,
|
||||
|
|
@ -194,7 +196,9 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
|||
projectId,
|
||||
{
|
||||
entity_identifier: issueId ?? "",
|
||||
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
entity_type: isDraft
|
||||
? EFileAssetType.DRAFT_ISSUE_DESCRIPTION
|
||||
: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
<div className="px-5">
|
||||
<IssueDescriptionEditor
|
||||
control={control}
|
||||
isDraft={isDraft}
|
||||
issueName={watch("name")}
|
||||
issueId={data?.id}
|
||||
descriptionHtmlData={data?.description_html}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue