[WIKI-481] fix: duplicate attachments when template is used #3486

This commit is contained in:
Aaryan Khandelwal
2025-06-27 16:16:13 +05:30
committed by GitHub
parent 2c916d8456
commit 73042d026c
6 changed files with 65 additions and 20 deletions

View File

@@ -0,0 +1,46 @@
/**
* @description function to extract all additional assets from HTML content
* @param htmlContent
* @returns {string[]} array of additional asset sources
*/
export const extractAdditionalAssetsFromHTMLContent = (htmlContent: string): string[] => {
// create a DOM parser
const parser = new DOMParser();
// parse the HTML string into a DOM document
const doc = parser.parseFromString(htmlContent, "text/html");
// collect all unique asset sources
const assetSources = new Set<string>();
// extract sources from attachment components
const attachmentComponents = doc.querySelectorAll("attachment-component");
attachmentComponents.forEach((component) => {
const src = component.getAttribute("src");
if (src) assetSources.add(src);
});
return Array.from(assetSources);
};
/**
* @description function to replace additional assets in HTML content with new IDs
* @param props
* @returns {string} HTML content with replaced additional assets
*/
export const replaceAdditionalAssetsInHTMLContent = (props: {
htmlContent: string;
assetMap: Record<string, string>;
}): string => {
const { htmlContent, assetMap } = props;
// create a DOM parser
const parser = new DOMParser();
// parse the HTML string into a DOM document
const doc = parser.parseFromString(htmlContent, "text/html");
// replace sources in attachment components
const attachmentComponents = doc.querySelectorAll("attachment-component");
attachmentComponents.forEach((component) => {
const oldSrc = component.getAttribute("src");
if (oldSrc && assetMap[oldSrc]) {
component.setAttribute("src", assetMap[oldSrc]);
}
});
// serialize the document back into a string
return doc.body.innerHTML;
};

View File

@@ -1,17 +1,19 @@
import { FC, useRef, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { EIssueServiceType } from "@plane/constants";
import { cn } from "@plane/utils";
// components
import { TIssueOperations } from "@/components/issues";
// helpers
import { cn } from "@plane/utils";
// hooks
import { useIssueDetail } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress";
import usePeekOverviewOutsideClickDetector from "@/hooks/use-peek-overview-outside-click";
// local components
// plane web imports
import { useCustomers } from "@/plane-web/hooks/store";
import { useEpics } from "@/plane-web/hooks/store/epics/use-epics";
import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives";
// local imports
import { EpicDetailRoot } from "../details/root";
import { EpicPeekOverviewError } from "./error";
import { EpicPeekOverviewHeader, TPeekModes } from "./header";
@@ -95,8 +97,9 @@ export const EpicView: FC<IEpicView> = observer((props) => {
const handleKeyDown = () => {
const slashCommandDropdownElement = document.querySelector("#slash-command");
const editorImageFullScreenModalElement = document.querySelector(".editor-image-full-screen-modal");
const dropdownElement = document.activeElement?.tagName === "INPUT";
if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement) {
if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement && !editorImageFullScreenModalElement) {
removeRoutePeekId();
const issueElement = document.getElementById(`issue-${issueId}`);
if (issueElement) issueElement?.focus();

View File

@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import { Check, Search, Shapes } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { EditorTitleRefApi, getEditorContentWithReplacedImageAssets } from "@plane/editor";
import { EditorTitleRefApi, getEditorContentWithReplacedAssets } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { FileService } from "@plane/services";
import { TPageTemplate } from "@plane/types";
@@ -68,7 +68,7 @@ export const TemplateSelectModal: React.FC<TTemplateSelectModalProps> = observer
setIsApplyingTemplate(true);
// duplicate template assets and replace the old assets with the new ones
const duplicateAssetService = fileService.duplicateAssets.bind(fileService, workspaceSlug);
const documentPayload = await getEditorContentWithReplacedImageAssets({
const documentPayload = await getEditorContentWithReplacedAssets({
descriptionHTML: selectedTemplate.template_data.description_html ?? "",
entityId: page.id,
entityType: EFileAssetType.PAGE_DESCRIPTION,

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { EIssuesStoreType, ETemplateLevel, ETemplateType } from "@plane/constants";
import { getEditorContentWithReplacedImageAssets } from "@plane/editor";
import { getEditorContentWithReplacedAssets } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { FileService } from "@plane/services";
import { TPage } from "@plane/types";
@@ -107,7 +107,7 @@ export const TemplateListActionWrapper = observer((props: TTemplateListActionWra
// duplicate the assets
const duplicateAssetService = fileService.duplicateAssets.bind(fileService, workspaceSlug);
const documentPayload = await getEditorContentWithReplacedImageAssets({
const documentPayload = await getEditorContentWithReplacedAssets({
descriptionHTML: pageData.description_html ?? "",
entityId: page.id,
entityType: EFileAssetType.PAGE_DESCRIPTION,

View File

@@ -135,13 +135,11 @@ export const TemplateBasicDetails = observer(
<RichTextEditor
id="template-publish-description"
initialValue={value ?? "<p></p>"}
workspaceSlug={workspaceSlug}
workspaceSlug={workspaceSlug.toString()}
workspaceId={workspaceId}
projectId={projectId ? projectId : undefined}
projectId={projectId ?? undefined}
ref={editorRef}
onChange={(description_json: object, description_html: string) => {
onChange(description_html);
}}
onChange={(_description_json, description_html) => onChange(description_html)}
searchMentionCallback={searchEntity}
placeholder={(isFocused, value) =>
isEditorEmpty(value)
@@ -149,7 +147,7 @@ export const TemplateBasicDetails = observer(
: t(`${getDescriptionPlaceholderI18n(isFocused, value)}`)
}
containerClassName="min-h-[240px] md:min-h-[120px] border-[0.5px] py-2 border-custom-border-200 rounded-md"
disabledExtensions={["image", "issue-embed"]}
disabledExtensions={["image", "issue-embed", "attachments"]}
uploadFile={() => Promise.resolve("")}
/>
)}

View File

@@ -106,21 +106,19 @@ export const WorkItemDetails = observer((props: TWorkItemDetailsProps) => {
<RichTextEditor
id="work-item-description"
initialValue={value ?? "<p></p>"}
workspaceSlug={workspaceSlug as string}
workspaceSlug={workspaceSlug.toString()}
workspaceId={workspaceId}
projectId={projectId ? projectId : undefined}
projectId={projectId ?? undefined}
ref={editorRef}
searchMentionCallback={searchEntity}
onChange={(description_json: object, description_html: string) => {
onChange(description_html);
}}
onChange={(_description_json, description_html) => onChange(description_html)}
placeholder={(isFocused, value) =>
isEditorEmpty(value)
? t("templates.settings.form.work_item.description.placeholder")
: t(`${getDescriptionPlaceholderI18n(isFocused, value)}`)
}
containerClassName="min-h-[80px] px-0"
disabledExtensions={["image"]}
disabledExtensions={["image", "attachments"]}
uploadFile={async (blockId, file) => {
try {
const { asset_id } = await uploadEditorAsset({