mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 14:01:45 +02:00
[WIKI-481] fix: duplicate attachments when template is used #3486
This commit is contained in:
committed by
GitHub
parent
2c916d8456
commit
73042d026c
46
packages/editor/src/ee/helpers/parser.ts
Normal file
46
packages/editor/src/ee/helpers/parser.ts
Normal 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;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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("")}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user