diff --git a/apps/api/plane/app/serializers/issue.py b/apps/api/plane/app/serializers/issue.py index 673a5570616..0d75a1c6ef6 100644 --- a/apps/api/plane/app/serializers/issue.py +++ b/apps/api/plane/app/serializers/issue.py @@ -97,6 +97,12 @@ class IssueCreateSerializer(BaseSerializer): write_only=True, required=False, ) + cover_image_attachment_id = serializers.PrimaryKeyRelatedField( + source="cover_image_attachment", + queryset=FileAsset.objects.all(), + required=False, + allow_null=True, + ) project_id = serializers.UUIDField(source="project.id", read_only=True) workspace_id = serializers.UUIDField(source="workspace.id", read_only=True) @@ -194,6 +200,21 @@ def validate(self, attrs): ): raise serializers.ValidationError("Estimate point is not valid please pass a valid estimate_point_id") + # Cover image must be an image attachment belonging to this issue. A new + # issue has no attachments yet, so a cover can only be set on update. + if attrs.get("cover_image_attachment"): + cover = attrs["cover_image_attachment"] + if ( + self.instance is None + or cover.entity_type != FileAsset.EntityTypeContext.ISSUE_ATTACHMENT + or str(cover.issue_id) != str(self.instance.id) + or str(cover.project_id) != str(self.context.get("project_id")) + or not str(cover.attributes.get("type", "")).startswith("image/") + ): + raise serializers.ValidationError( + "Cover image must be an image attachment belonging to this work item" + ) + return attrs def create(self, validated_data): @@ -800,6 +821,7 @@ class Meta: "link_count", "is_draft", "archived_at", + "cover_image_attachment_id", ] read_only_fields = fields diff --git a/apps/api/plane/app/views/issue/attachment.py b/apps/api/plane/app/views/issue/attachment.py index 51248b8a428..30f086b0c8a 100644 --- a/apps/api/plane/app/views/issue/attachment.py +++ b/apps/api/plane/app/views/issue/attachment.py @@ -20,7 +20,7 @@ # Module imports from .. import BaseAPIView from plane.app.serializers import IssueAttachmentSerializer -from plane.db.models import FileAsset, Workspace +from plane.db.models import FileAsset, Issue, Workspace from plane.bgtasks.issue_activities_task import issue_activity from plane.app.permissions import allow_permission, ROLE from plane.settings.storage import S3Storage @@ -153,6 +153,10 @@ def delete(self, request, slug, project_id, issue_id, pk): issue_attachment.deleted_at = timezone.now() issue_attachment.save() + # Soft-deleting the asset does not trip the cover FK's on_delete, so any + # work item using it as its cover would keep a dangling reference. Clear it. + Issue.objects.filter(cover_image_attachment_id=pk).update(cover_image_attachment=None) + issue_activity.delay( type="attachment.activity.deleted", requested_data=None, diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index d9e2ea5a5a8..c1a1b781eaf 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -187,6 +187,7 @@ def get(self, request, slug, project_id): "is_draft", "archived_at", "deleted_at", + "cover_image_attachment_id", ) datetime_fields = ["created_at", "updated_at"] issues = user_timezone_converter(issues, datetime_fields, request.user.user_timezone) @@ -452,6 +453,7 @@ def create(self, request, slug, project_id): "is_draft", "archived_at", "deleted_at", + "cover_image_attachment_id", ) .first() ) @@ -883,6 +885,7 @@ def list(self, request, slug, project_id): "link_count", "attachment_count", "sub_issues_count", + "cover_image_attachment_id", ] if str(is_description_required).lower() == "true": diff --git a/apps/api/plane/app/views/issue/sub_issue.py b/apps/api/plane/app/views/issue/sub_issue.py index 5194148dd7b..0abaacad3e2 100644 --- a/apps/api/plane/app/views/issue/sub_issue.py +++ b/apps/api/plane/app/views/issue/sub_issue.py @@ -165,6 +165,7 @@ def get(self, request, slug, project_id, issue_id): "is_draft", "archived_at", "state_group", + "cover_image_attachment_id", ) ) diff --git a/apps/api/plane/db/migrations/0122_issue_cover_image_attachment.py b/apps/api/plane/db/migrations/0122_issue_cover_image_attachment.py new file mode 100644 index 00000000000..6bd17e2affd --- /dev/null +++ b/apps/api/plane/db/migrations/0122_issue_cover_image_attachment.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.29 on 2026-05-28 20:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0121_alter_estimate_type'), + ] + + operations = [ + migrations.AddField( + model_name='issue', + name='cover_image_attachment', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cover_image_issues', to='db.fileasset'), + ), + ] diff --git a/apps/api/plane/db/models/issue.py b/apps/api/plane/db/models/issue.py index fe23ee681dc..6d26ac394eb 100644 --- a/apps/api/plane/db/models/issue.py +++ b/apps/api/plane/db/models/issue.py @@ -168,6 +168,13 @@ class Issue(ChangeTrackerMixin, ProjectBaseModel): null=True, blank=True, ) + cover_image_attachment = models.ForeignKey( + "db.FileAsset", + on_delete=models.SET_NULL, + related_name="cover_image_issues", + null=True, + blank=True, + ) issue_objects = IssueManager() diff --git a/apps/api/plane/utils/grouper.py b/apps/api/plane/utils/grouper.py index ab008796715..d8fc72a25ad 100644 --- a/apps/api/plane/utils/grouper.py +++ b/apps/api/plane/utils/grouper.py @@ -127,6 +127,7 @@ def issue_on_results( "is_draft", "archived_at", "state__group", + "cover_image_attachment_id", ] if group_by in FIELD_MAPPER: diff --git a/apps/web/core/components/issues/attachment/attachment-item-list.tsx b/apps/web/core/components/issues/attachment/attachment-item-list.tsx index 0ac4db84f29..a61efc99cc2 100644 --- a/apps/web/core/components/issues/attachment/attachment-item-list.tsx +++ b/apps/web/core/components/issues/attachment/attachment-item-list.tsx @@ -10,7 +10,7 @@ import type { FileRejection } from "react-dropzone"; import { useDropzone } from "react-dropzone"; import { UploadCloud } from "lucide-react"; import { useTranslation } from "@plane/i18n"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; +import { TOAST_TYPE, setToast, setPromiseToast } from "@plane/propel/toast"; import type { TIssueServiceType } from "@plane/types"; import { EIssueServiceType } from "@plane/types"; // hooks @@ -53,6 +53,7 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList toggleDeleteAttachmentModal, fetchActivities, } = useIssueDetail(issueServiceType); + const { updateIssue, getIssueById } = useIssueDetail(issueServiceType).issue; const { operations: attachmentOperations, snapshot: attachmentSnapshot } = attachmentHelpers; const { create: createAttachment } = attachmentOperations; const { uploadStatus } = attachmentSnapshot; @@ -60,12 +61,43 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList const { maxFileSize } = useFileSize(); // derived values const issueAttachments = getAttachmentsByIssueId(issueId); + const coverImageAttachmentId = getIssueById(issueId)?.cover_image_attachment_id ?? null; // handlers const handleFetchPropertyActivities = useCallback(() => { fetchActivities(workspaceSlug, projectId, issueId); }, [fetchActivities, workspaceSlug, projectId, issueId]); + const handleToggleCoverImage = useCallback( + async (attachmentId: string) => { + const isCurrentCover = getIssueById(issueId)?.cover_image_attachment_id === attachmentId; + const nextCoverId = isCurrentCover ? null : attachmentId; + const coverPromise = updateIssue(workspaceSlug, projectId, issueId, { + cover_image_attachment_id: nextCoverId, + }).then(() => { + handleFetchPropertyActivities(); + return; + }); + + setPromiseToast(coverPromise, { + loading: isCurrentCover ? t("attachment.remove_cover_loading") : t("attachment.set_cover_loading"), + success: { + title: isCurrentCover ? t("attachment.remove_cover_success_title") : t("attachment.set_cover_success_title"), + message: () => + isCurrentCover ? t("attachment.remove_cover_success_message") : t("attachment.set_cover_success_message"), + }, + error: { + title: isCurrentCover ? t("attachment.remove_cover_error_title") : t("attachment.set_cover_error_title"), + message: () => + isCurrentCover ? t("attachment.remove_cover_error_message") : t("attachment.set_cover_error_message"), + }, + }); + + return coverPromise; + }, + [updateIssue, getIssueById, workspaceSlug, projectId, issueId, handleFetchPropertyActivities, t] + ); + const onDrop = useCallback( (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { const totalAttachedFiles = acceptedFiles.length + rejectedFiles.length; @@ -100,7 +132,7 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList }); return; }, - [createAttachment, maxFileSize, workspaceSlug, handleFetchPropertyActivities] + [createAttachment, maxFileSize, workspaceSlug, handleFetchPropertyActivities, t] ); const { getRootProps, getInputProps, isDragActive } = useDropzone({ @@ -112,8 +144,8 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList return ( <> - {uploadStatus?.map((uploadStatus) => ( - + {uploadStatus?.map((status) => ( + ))} {issueAttachments && ( <> @@ -147,6 +179,8 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList attachmentId={attachmentId} disabled={disabled} issueServiceType={issueServiceType} + onToggleCoverImage={handleToggleCoverImage} + isCoverImage={coverImageAttachmentId === attachmentId} /> ))} diff --git a/apps/web/core/components/issues/attachment/attachment-list-item.tsx b/apps/web/core/components/issues/attachment/attachment-list-item.tsx index 6c8135d6264..dfd2d1c3a76 100644 --- a/apps/web/core/components/issues/attachment/attachment-list-item.tsx +++ b/apps/web/core/components/issues/attachment/attachment-list-item.tsx @@ -5,6 +5,7 @@ */ import { observer } from "mobx-react"; +import { Image, ImageOff } from "lucide-react"; import { useTranslation } from "@plane/i18n"; import { TrashIcon } from "@plane/propel/icons"; @@ -28,12 +29,20 @@ type TIssueAttachmentsListItem = { attachmentId: string; disabled?: boolean; issueServiceType?: TIssueServiceType; + onToggleCoverImage?: (attachmentId: string) => Promise; + isCoverImage?: boolean; }; export const IssueAttachmentsListItem = observer(function IssueAttachmentsListItem(props: TIssueAttachmentsListItem) { const { t } = useTranslation(); // props - const { attachmentId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props; + const { + attachmentId, + disabled, + issueServiceType = EIssueServiceType.ISSUES, + onToggleCoverImage, + isCoverImage = false, + } = props; // store hooks const { getUserDetails } = useMember(); const { @@ -46,6 +55,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt const fileExtension = getFileExtension(attachment?.attributes.name ?? ""); const fileIcon = getFileIcon(fileExtension, 18); const fileURL = getFileURL(attachment?.asset_url ?? ""); + const isImage = attachment?.attributes.name ? /\.(jpg|jpeg|png|gif|webp)$/i.test(attachment.attributes.name) : false; // hooks const { isMobile } = usePlatformOS(); @@ -87,6 +97,22 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt )} + {isImage && onToggleCoverImage && ( + { + onToggleCoverImage(attachmentId); + }} + > +
+ {isCoverImage ? ( + + ) : ( + + )} + {isCoverImage ? t("attachment.remove_cover_image") : t("attachment.make_cover_image")} +
+
+ )} { toggleDeleteAttachmentModal(attachmentId); diff --git a/apps/web/core/components/issues/issue-detail/cover-image.tsx b/apps/web/core/components/issues/issue-detail/cover-image.tsx new file mode 100644 index 00000000000..83eb44b26d1 --- /dev/null +++ b/apps/web/core/components/issues/issue-detail/cover-image.tsx @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { cn } from "@plane/utils"; +import { useIssueCoverImage } from "@/hooks/use-issue-cover-image"; + +interface IssueDetailCoverImageProps { + issueId: string; + projectId: string | null; + coverImageAttachmentId?: string | null; + // Negative margins must cancel the parent container's horizontal padding so the + // cover bleeds edge-to-edge. Defaults assume a px-8 container (peek overview); + // pass a matching value for other paddings (e.g. px-9 on the browse view). + layoutClassName?: string; +} + +export const IssueDetailCoverImage = observer(function IssueDetailCoverImage(props: IssueDetailCoverImageProps) { + const { issueId, projectId, coverImageAttachmentId, layoutClassName = "-mx-8 w-[calc(100%+4rem)]" } = props; + const { workspaceSlug } = useParams(); + const [imageLoadError, setImageLoadError] = useState(false); + + const coverImageUrl = useIssueCoverImage(workspaceSlug?.toString(), projectId, issueId, coverImageAttachmentId); + + useEffect(() => { + setImageLoadError(false); + }, [coverImageUrl]); + + if (!coverImageUrl || imageLoadError) { + return null; + } + + return ( +
+ Cover setImageLoadError(true)} + loading="lazy" + /> +
+ ); +}); diff --git a/apps/web/core/components/issues/issue-detail/main-content.tsx b/apps/web/core/components/issues/issue-detail/main-content.tsx index d14bd35396a..988ac595dca 100644 --- a/apps/web/core/components/issues/issue-detail/main-content.tsx +++ b/apps/web/core/components/issues/issue-detail/main-content.tsx @@ -32,6 +32,7 @@ import { IssueDetailWidgets } from "../issue-detail-widgets"; import { NameDescriptionUpdateStatus } from "../issue-update-status"; import { PeekOverviewProperties } from "../peek-overview/properties"; import { IssueTitleInput } from "../title-input"; +import { IssueDetailCoverImage } from "./cover-image"; import { IssueActivity } from "./issue-activity"; import { IssueParentDetail } from "./parent"; import { IssueReaction } from "./reactions"; @@ -92,6 +93,12 @@ export const IssueMainContent = observer(function IssueMainContent(props: Props) return ( <> +
{issue.parent_id && ( handleIssuePeekOverview(issue)} disabled={!!issue?.tempId} > - - - + +
+ + + +
diff --git a/apps/web/core/components/issues/issue-layouts/kanban/cover-image.tsx b/apps/web/core/components/issues/issue-layouts/kanban/cover-image.tsx new file mode 100644 index 00000000000..50eb4d779cc --- /dev/null +++ b/apps/web/core/components/issues/issue-layouts/kanban/cover-image.tsx @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { useIssueCoverImage } from "@/hooks/use-issue-cover-image"; + +interface KanbanIssueCoverImageProps { + issueId: string; + projectId: string | null; + coverImageAttachmentId?: string | null; +} + +export const KanbanIssueCoverImage = observer(function KanbanIssueCoverImage(props: KanbanIssueCoverImageProps) { + const { issueId, projectId, coverImageAttachmentId } = props; + const { workspaceSlug } = useParams(); + const [imageLoadError, setImageLoadError] = useState(false); + + const coverImageUrl = useIssueCoverImage(workspaceSlug?.toString(), projectId, issueId, coverImageAttachmentId); + + useEffect(() => { + setImageLoadError(false); + }, [coverImageUrl]); + + if (!coverImageUrl || imageLoadError) { + return null; + } + + return ( +
+ Cover setImageLoadError(true)} + loading="lazy" + /> +
+ ); +}); diff --git a/apps/web/core/components/issues/peek-overview/issue-detail.tsx b/apps/web/core/components/issues/peek-overview/issue-detail.tsx index 6bd89124b62..dfb7ac92a83 100644 --- a/apps/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/apps/web/core/components/issues/peek-overview/issue-detail.tsx @@ -30,6 +30,7 @@ import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-dup import { WorkItemVersionService } from "@/services/issue"; // local components import type { TIssueOperations } from "../issue-detail"; +import { IssueDetailCoverImage } from "../issue-detail/cover-image"; import { IssueParentDetail } from "../issue-detail/parent"; import { IssueReaction } from "../issue-detail/reactions"; import { IssueTitleInput } from "../title-input"; @@ -97,101 +98,104 @@ export const PeekOverviewIssueDetails = observer(function PeekOverviewIssueDetai : undefined; return ( -
- {issue.parent_id && ( - - )} -
- - {duplicateIssues?.length > 0 && ( - - )} -
- + setIsSubmitting(value)} - issueOperations={issueOperations} - disabled={disabled || isArchived} - value={issue.name} - containerClassName="-ml-3" + coverImageAttachmentId={issue.cover_image_attachment_id} /> - - { - if (!issue.id || !issue.project_id) return; - await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { - description_html: value.description_html, - ...(isMigrationUpdate ? { skip_activity: "true" } : {}), - }); - }} - setIsSubmitting={(value) => setIsSubmitting(value)} - projectId={issue.project_id} - workspaceSlug={workspaceSlug} - /> - -
- {currentUser && ( - + {issue.parent_id && ( + - )} - {!disabled && ( - - workItemVersionService.listDescriptionVersions( - workspaceSlug, - issue.project_id?.toString() ?? "", - issueId - ), - retrieveDescriptionVersion: (issueId, versionId) => - workItemVersionService.retrieveDescriptionVersion( - workspaceSlug, - issue.project_id?.toString() ?? "", - issueId, - versionId - ), - }} - handleRestore={(descriptionHTML) => editorRef.current?.setEditorValue(descriptionHTML, true)} - projectId={issue.project_id} - workspaceSlug={workspaceSlug} + issue={issue} + issueOperations={issueOperations} /> )} +
+ + {duplicateIssues?.length > 0 && ( + + )} +
+ setIsSubmitting(value)} + issueOperations={issueOperations} + disabled={disabled || isArchived} + value={issue.name} + containerClassName="-ml-3" + /> + + { + if (!issue.id || !issue.project_id) return; + await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { + description_html: value.description_html, + ...(isMigrationUpdate ? { skip_activity: "true" } : {}), + }); + }} + setIsSubmitting={(value) => setIsSubmitting(value)} + projectId={issue.project_id} + workspaceSlug={workspaceSlug} + /> + +
+ {currentUser && ( + + )} + {!disabled && ( + + workItemVersionService.listDescriptionVersions(workspaceSlug, issue.project_id?.toString() ?? "", id), + retrieveDescriptionVersion: (id, versionId) => + workItemVersionService.retrieveDescriptionVersion( + workspaceSlug, + issue.project_id?.toString() ?? "", + id, + versionId + ), + }} + handleRestore={(descriptionHTML) => editorRef.current?.setEditorValue(descriptionHTML, true)} + projectId={issue.project_id} + workspaceSlug={workspaceSlug} + /> + )} +
-
+ ); }); diff --git a/apps/web/core/hooks/use-issue-cover-image.ts b/apps/web/core/hooks/use-issue-cover-image.ts new file mode 100644 index 00000000000..8a792ea2d6e --- /dev/null +++ b/apps/web/core/hooks/use-issue-cover-image.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { getFileURL } from "@plane/utils"; + +// Mirrors FileAsset.asset_url for ISSUE_ATTACHMENT on the backend, letting us +// resolve the cover from its attachment id without fetching the attachment list. +const buildAttachmentUrl = (workspaceSlug: string, projectId: string, issueId: string, attachmentId: string) => + getFileURL( + `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/attachments/${attachmentId}/` + ) ?? null; + +export const useIssueCoverImage = ( + workspaceSlug: string | undefined, + projectId: string | null | undefined, + issueId: string, + coverImageAttachmentId?: string | null +): string | null => { + if (!workspaceSlug || !projectId || !issueId || !coverImageAttachmentId) return null; + return buildAttachmentUrl(workspaceSlug, projectId, issueId, coverImageAttachmentId); +}; diff --git a/apps/web/core/store/issue/issue-details/attachment.store.ts b/apps/web/core/store/issue/issue-details/attachment.store.ts index c9eeb1158f0..dcecb705151 100644 --- a/apps/web/core/store/issue/issue-details/attachment.store.ts +++ b/apps/web/core/store/issue/issue-details/attachment.store.ts @@ -185,6 +185,8 @@ export class IssueAttachmentStore implements IIssueAttachmentStore { }; removeAttachment = async (workspaceSlug: string, projectId: string, issueId: string, attachmentId: string) => { + const wasCoverImage = this.rootIssueStore.issues.getIssueById(issueId)?.cover_image_attachment_id === attachmentId; + const response = await this.issueAttachmentService.deleteIssueAttachment( workspaceSlug, projectId, @@ -198,8 +200,11 @@ export class IssueAttachmentStore implements IIssueAttachmentStore { return attachmentIds; }); delete this.attachmentMap[attachmentId]; + // The backend clears cover_image_attachment_id when the asset is deleted, + // so just mirror that locally to avoid leaving a dangling cover reference. this.rootIssueStore.issues.updateIssue(issueId, { attachment_count: this.getAttachmentsCountByIssueId(issueId), + ...(wasCoverImage ? { cover_image_attachment_id: null } : {}), }); }); diff --git a/apps/web/core/store/issue/issue-details/issue.store.ts b/apps/web/core/store/issue/issue-details/issue.store.ts index 4abf62b4f5c..74ca8878bba 100644 --- a/apps/web/core/store/issue/issue-details/issue.store.ts +++ b/apps/web/core/store/issue/issue-details/issue.store.ts @@ -159,6 +159,7 @@ export class IssueStore implements IIssueStore { cycle_id: issue?.cycle_id, module_ids: issue?.module_ids, type_id: issue?.type_id, + cover_image_attachment_id: issue?.cover_image_attachment_id, created_at: issue?.created_at, updated_at: issue?.updated_at, start_date: issue?.start_date, diff --git a/packages/i18n/src/locales/cs/common.json b/packages/i18n/src/locales/cs/common.json index c1e9372c24e..6c4b5baca21 100644 --- a/packages/i18n/src/locales/cs/common.json +++ b/packages/i18n/src/locales/cs/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Je možné nahrát pouze jeden soubor najednou.", "file_size_limit": "Soubor musí být menší než {size}MB.", "drag_and_drop": "Přetáhněte soubor kamkoli pro nahrání", - "delete": "Smazat přílohu" + "delete": "Smazat přílohu", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Vybrat štítek", diff --git a/packages/i18n/src/locales/de/common.json b/packages/i18n/src/locales/de/common.json index 1e18d4ee26e..4293393f691 100644 --- a/packages/i18n/src/locales/de/common.json +++ b/packages/i18n/src/locales/de/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Es kann jeweils nur eine Datei hochgeladen werden.", "file_size_limit": "Die Datei muss kleiner als {size} MB sein.", "drag_and_drop": "Datei hierher ziehen, um sie hochzuladen", - "delete": "Anhang löschen" + "delete": "Anhang löschen", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Label auswählen", diff --git a/packages/i18n/src/locales/en/common.json b/packages/i18n/src/locales/en/common.json index a138304371e..7036ac154f0 100644 --- a/packages/i18n/src/locales/en/common.json +++ b/packages/i18n/src/locales/en/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Only one file can be uploaded at a time.", "file_size_limit": "File must be of {size}MB or less in size.", "drag_and_drop": "Drag and drop anywhere to upload", - "delete": "Delete attachment" + "delete": "Delete attachment", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Add labels", diff --git a/packages/i18n/src/locales/es/common.json b/packages/i18n/src/locales/es/common.json index 64440c9c653..74061740def 100644 --- a/packages/i18n/src/locales/es/common.json +++ b/packages/i18n/src/locales/es/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Solo se puede subir un archivo a la vez.", "file_size_limit": "El archivo debe tener {size}MB o menos de tamaño.", "drag_and_drop": "Arrastra y suelta en cualquier lugar para subir", - "delete": "Eliminar archivo adjunto" + "delete": "Eliminar archivo adjunto", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Seleccionar etiqueta", diff --git a/packages/i18n/src/locales/fr/common.json b/packages/i18n/src/locales/fr/common.json index a94ab8aba2d..2f8b12740bf 100644 --- a/packages/i18n/src/locales/fr/common.json +++ b/packages/i18n/src/locales/fr/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Un seul fichier peut être téléchargé à la fois.", "file_size_limit": "Le fichier doit faire {size}MB ou moins.", "drag_and_drop": "Glissez-déposez n’importe où pour uploader", - "delete": "Supprimer la pièce jointe" + "delete": "Supprimer la pièce jointe", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Sélectionner une étiquette", diff --git a/packages/i18n/src/locales/id/common.json b/packages/i18n/src/locales/id/common.json index 7d266850dc4..b8086744daa 100644 --- a/packages/i18n/src/locales/id/common.json +++ b/packages/i18n/src/locales/id/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Hanya satu file yang dapat diunggah pada satu waktu.", "file_size_limit": "File harus berukuran {size}MB atau lebih kecil.", "drag_and_drop": "Seret dan jatuhkan di mana saja untuk mengunggah", - "delete": "Hapus lampiran" + "delete": "Hapus lampiran", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Pilih label", diff --git a/packages/i18n/src/locales/it/common.json b/packages/i18n/src/locales/it/common.json index 6fda1b04957..b7dbd47d66a 100644 --- a/packages/i18n/src/locales/it/common.json +++ b/packages/i18n/src/locales/it/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "È possibile caricare un solo file alla volta.", "file_size_limit": "Il file deve essere di {size}MB o meno.", "drag_and_drop": "Trascina e rilascia ovunque per caricare", - "delete": "Elimina allegato" + "delete": "Elimina allegato", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Seleziona etichetta", diff --git a/packages/i18n/src/locales/ja/common.json b/packages/i18n/src/locales/ja/common.json index 4899411b615..219d3a34b3a 100644 --- a/packages/i18n/src/locales/ja/common.json +++ b/packages/i18n/src/locales/ja/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "一度にアップロードできるファイルは1つだけです。", "file_size_limit": "ファイルサイズは{size}MB以下である必要があります。", "drag_and_drop": "どこにでもドラッグ&ドロップでアップロード", - "delete": "添付ファイルを削除" + "delete": "添付ファイルを削除", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "ラベルを選択", diff --git a/packages/i18n/src/locales/ko/common.json b/packages/i18n/src/locales/ko/common.json index 73eea6b1c25..f528e7ffea5 100644 --- a/packages/i18n/src/locales/ko/common.json +++ b/packages/i18n/src/locales/ko/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "한 번에 하나의 파일만 업로드할 수 있습니다.", "file_size_limit": "파일 크기는 {size}MB 이하이어야 합니다.", "drag_and_drop": "업로드하려면 아무 곳에나 드래그 앤 드롭하세요", - "delete": "첨부 파일 삭제" + "delete": "첨부 파일 삭제", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "레이블 선택", diff --git a/packages/i18n/src/locales/pl/common.json b/packages/i18n/src/locales/pl/common.json index 4c4850cbf66..03f7b7c6075 100644 --- a/packages/i18n/src/locales/pl/common.json +++ b/packages/i18n/src/locales/pl/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Możesz przesłać tylko jeden plik naraz.", "file_size_limit": "Plik musi być mniejszy niż {size}MB.", "drag_and_drop": "Przeciągnij plik w dowolne miejsce, aby przesłać", - "delete": "Usuń załącznik" + "delete": "Usuń załącznik", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Wybierz etykietę", diff --git a/packages/i18n/src/locales/pt-BR/common.json b/packages/i18n/src/locales/pt-BR/common.json index 565e85d8739..d3b423bf306 100644 --- a/packages/i18n/src/locales/pt-BR/common.json +++ b/packages/i18n/src/locales/pt-BR/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Apenas um arquivo pode ser enviado por vez.", "file_size_limit": "O arquivo deve ter {size}MB ou menos.", "drag_and_drop": "Arraste e solte em qualquer lugar para enviar", - "delete": "Excluir anexo" + "delete": "Excluir anexo", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Selecionar etiqueta", diff --git a/packages/i18n/src/locales/ro/common.json b/packages/i18n/src/locales/ro/common.json index e2b44ce87ec..e66f8a60536 100644 --- a/packages/i18n/src/locales/ro/common.json +++ b/packages/i18n/src/locales/ro/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Se poate încărca doar un fișier o dată.", "file_size_limit": "Fișierul trebuie să aibă {size}MB sau mai puțin.", "drag_and_drop": "Trage și plasează oriunde pentru a încărca", - "delete": "Șterge atașamentul" + "delete": "Șterge atașamentul", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Selectează eticheta", diff --git a/packages/i18n/src/locales/ru/common.json b/packages/i18n/src/locales/ru/common.json index c2881e55f8a..4971704b314 100644 --- a/packages/i18n/src/locales/ru/common.json +++ b/packages/i18n/src/locales/ru/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Можно загрузить только один файл", "file_size_limit": "Максимальный размер файла - {size} МБ", "drag_and_drop": "Перетащите файл для загрузки", - "delete": "Удалить вложение" + "delete": "Удалить вложение", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Выбрать метку", diff --git a/packages/i18n/src/locales/sk/common.json b/packages/i18n/src/locales/sk/common.json index 4fc339cece6..76546c42930 100644 --- a/packages/i18n/src/locales/sk/common.json +++ b/packages/i18n/src/locales/sk/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Je možné nahrať iba jeden súbor naraz.", "file_size_limit": "Súbor musí byť menší ako {size}MB.", "drag_and_drop": "Pretiahnite súbor kamkoľvek pre nahratie", - "delete": "Zmazať prílohu" + "delete": "Zmazať prílohu", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Vybrať štítok", diff --git a/packages/i18n/src/locales/tr-TR/common.json b/packages/i18n/src/locales/tr-TR/common.json index 74bd5cbf9b3..85e54bd3649 100644 --- a/packages/i18n/src/locales/tr-TR/common.json +++ b/packages/i18n/src/locales/tr-TR/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Aynı anda yalnızca bir dosya yüklenebilir.", "file_size_limit": "Dosya boyutu {size}MB veya daha az olmalıdır.", "drag_and_drop": "Yüklemek için herhangi bir yere sürükleyip bırakın", - "delete": "Eki sil" + "delete": "Eki sil", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Etiket seç", diff --git a/packages/i18n/src/locales/ua/common.json b/packages/i18n/src/locales/ua/common.json index 89bd906d8fa..032963b10f5 100644 --- a/packages/i18n/src/locales/ua/common.json +++ b/packages/i18n/src/locales/ua/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Можна завантажити лише один файл одночасно.", "file_size_limit": "Файл має бути меншим за {size}МБ.", "drag_and_drop": "Перетягніть файл сюди для завантаження", - "delete": "Видалити вкладення" + "delete": "Видалити вкладення", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Вибрати мітку", diff --git a/packages/i18n/src/locales/vi-VN/common.json b/packages/i18n/src/locales/vi-VN/common.json index f9aae43c1b9..8ab6312ba0b 100644 --- a/packages/i18n/src/locales/vi-VN/common.json +++ b/packages/i18n/src/locales/vi-VN/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "Chỉ có thể tải lên một tệp mỗi lần.", "file_size_limit": "Kích thước tệp phải nhỏ hơn hoặc bằng {size}MB.", "drag_and_drop": "Kéo và thả vào bất kỳ đâu để tải lên", - "delete": "Xóa tệp đính kèm" + "delete": "Xóa tệp đính kèm", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "Chọn nhãn", diff --git a/packages/i18n/src/locales/zh-CN/common.json b/packages/i18n/src/locales/zh-CN/common.json index dd67d925a07..91e62688f0e 100644 --- a/packages/i18n/src/locales/zh-CN/common.json +++ b/packages/i18n/src/locales/zh-CN/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "一次只能上传一个文件。", "file_size_limit": "文件大小必须小于或等于 {size}MB。", "drag_and_drop": "拖放到任意位置以上传", - "delete": "删除附件" + "delete": "删除附件", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "选择标签", diff --git a/packages/i18n/src/locales/zh-TW/common.json b/packages/i18n/src/locales/zh-TW/common.json index 834a2d922d8..81d4238deb4 100644 --- a/packages/i18n/src/locales/zh-TW/common.json +++ b/packages/i18n/src/locales/zh-TW/common.json @@ -762,7 +762,19 @@ "only_one_file_allowed": "一次只能上傳一個檔案。", "file_size_limit": "檔案大小必須小於或等於 {size}MB。", "drag_and_drop": "拖曳到任何位置以上傳", - "delete": "刪除附件" + "delete": "刪除附件", + "make_cover_image": "Make cover image", + "remove_cover_image": "Remove cover image", + "set_cover_loading": "Setting cover image...", + "set_cover_success_title": "Cover image set", + "set_cover_success_message": "This image is now the cover image", + "set_cover_error_title": "Couldn't set cover image", + "set_cover_error_message": "Could not set this image as the cover", + "remove_cover_loading": "Removing cover image...", + "remove_cover_success_title": "Cover image removed", + "remove_cover_success_message": "This work item no longer has a cover image", + "remove_cover_error_title": "Couldn't remove cover image", + "remove_cover_error_message": "Could not remove the cover image" }, "label": { "select": "選擇標籤", diff --git a/packages/types/src/issues/issue.ts b/packages/types/src/issues/issue.ts index 8054b4c44c3..398ea5f199b 100644 --- a/packages/types/src/issues/issue.ts +++ b/packages/types/src/issues/issue.ts @@ -63,6 +63,7 @@ export type TBaseIssue = { cycle_id: string | null; module_ids: string[] | null; type_id: string | null; + cover_image_attachment_id: string | null; created_at: string; updated_at: string;