feat(SW-2541): Changed asset types to only add the data needed

This commit is contained in:
Erik Tiekstra
2025-09-05 11:25:16 +02:00
parent 64556d4b9c
commit 1337e8293f
7 changed files with 243 additions and 310 deletions

View File

@@ -1,154 +1,65 @@
import {
Button,
ButtonGroup,
Field,
Field as FieldComponent,
FieldLabel,
ModalBody,
ModalFooter,
ModalHeader,
Select,
TextInput,
} from "@contentstack/venus-components"
import React, { ChangeEvent, useEffect, useState } from "react"
import { Path } from "slate"
import type {
IRteElementType,
IRteParam,
} from "@contentstack/app-sdk/dist/src/RTE/types"
import type { IRteElementType } from "@contentstack/app-sdk/dist/src/RTE/types"
import FocalPointPicker from "~/shared-components/FocalPointPicker"
import type { FocalPoint, InsertResponse } from "~/types/imagevault"
enum DropdownValues {
center = "center",
left = "left",
right = "right",
none = "none",
}
type DropDownItem = {
label: string
value: DropdownValues
type: string
}
const dropdownList: DropDownItem[] = [
{
label: "None",
value: DropdownValues.none,
type: "select",
},
{
label: "Center",
value: DropdownValues.center,
type: "select",
},
{
label: "Left",
value: DropdownValues.left,
type: "select",
},
{
label: "Right",
value: DropdownValues.right,
type: "select",
},
]
import type { FocalPoint, ImageVaultAsset } from "~/types/imagevault"
type ImageEditModalProps = {
element: IRteElementType & {
attrs: InsertResponse
attrs: ImageVaultAsset
}
rte: IRteParam
setData: (data: ImageVaultAsset) => void
closeModal: () => void
path: Path
}
export default function ImageEditModal({
element,
setData,
closeModal,
path,
rte,
}: ImageEditModalProps) {
const [alignment, setAlignment] = useState<DropDownItem>({
label: "None",
value: DropdownValues.none,
type: "select",
})
const imageVaultAsset: ImageVaultAsset = element.attrs
const [altText, setAltText] = useState("")
const [caption, setCaption] = useState("")
const [focalPoint, setFocalPoint] = useState<FocalPoint>({ x: 50, y: 50 })
const assetUrl = element.attrs.MediaConversions[0].Url
const assetUrl = imageVaultAsset.url
useEffect(() => {
if (element.attrs.Metadata && element.attrs.Metadata.length) {
const altText = element.attrs.Metadata.find((meta) =>
meta.Name.includes("AltText_")
)?.Value
const caption = element.attrs.Metadata.find((meta) =>
meta.Name.includes("Title_")
)?.Value
setAltText(altText ?? "")
setCaption(caption ?? "")
if (imageVaultAsset.meta.alt) {
setAltText(imageVaultAsset.meta.alt)
}
}, [element.attrs.Metadata])
if (imageVaultAsset.meta.caption) {
setCaption(imageVaultAsset.meta.caption)
}
}, [imageVaultAsset.meta])
useEffect(() => {
if (element.attrs.FocalPoint) {
setFocalPoint(element.attrs.FocalPoint)
if (imageVaultAsset.focalPoint) {
setFocalPoint(imageVaultAsset.focalPoint)
}
}, [element.attrs.FocalPoint])
}, [imageVaultAsset.focalPoint])
function handleSave() {
let newStyle
switch (alignment.value) {
case DropdownValues.center:
case DropdownValues.left:
case DropdownValues.right:
newStyle = {
textAlign: alignment.value,
maxWidth: element.attrs.width
? `${element.attrs.width}px`
: undefined,
}
break
case DropdownValues.none:
default:
newStyle = {}
break
}
const metaData = element.attrs.Metadata ?? []
const newMetadata = metaData.map((meta) => {
if (meta.Name.includes("AltText_")) {
return { ...meta, Value: altText }
}
if (meta.Name.includes("Title_")) {
return { ...meta, Value: caption }
}
return meta
})
rte._adv.Transforms?.setNodes<IRteElementType>(
rte._adv.editor,
{
attrs: {
...element.attrs,
Metadata: newMetadata,
position: alignment.value,
style: { ...element.attrs.style, ...newStyle },
FocalPoint: focalPoint,
},
const newData = {
...imageVaultAsset,
meta: {
alt: altText,
caption,
},
{ at: path }
)
focalPoint,
}
setData(newData)
closeModal()
}
@@ -177,18 +88,7 @@ export default function ImageEditModal({
onChange={changeFocalPoint}
/>
<div>
<Field>
{/* @ts-expect-error: Type incompatibility with Venus components */}
<Select
selectLabel="Alignment"
value={alignment}
onChange={(e: DropDownItem) => {
setAlignment(e)
}}
options={dropdownList}
/>
</Field>
<Field>
<FieldComponent>
<FieldLabel htmlFor="alt">Alt text</FieldLabel>
<TextInput
value={altText}
@@ -198,9 +98,9 @@ export default function ImageEditModal({
setAltText(e.target.value)
}
/>
</Field>
</FieldComponent>
<Field>
<FieldComponent>
<FieldLabel htmlFor="caption">Caption</FieldLabel>
<TextInput
value={caption}
@@ -210,16 +110,25 @@ export default function ImageEditModal({
setCaption(e.target.value)
}
/>
</Field>
</FieldComponent>
<Field>
<FieldComponent>
<FieldLabel htmlFor="focalPoint">Focal Point</FieldLabel>
<TextInput
value={`X: ${focalPoint.x}, Y: ${focalPoint.y}`}
name="focalPoint"
disabled
/>
</Field>
</FieldComponent>
<FieldComponent>
<FieldLabel htmlFor="imageVaultId">Imagevault Id</FieldLabel>
<TextInput
value={imageVaultAsset.imageVaultId}
name="imageVaultId"
disabled
/>
</FieldComponent>
</div>
</ModalBody>
<ModalFooter>

View File

@@ -1,5 +1,5 @@
import { Icon, Tooltip, cbModal } from "@contentstack/venus-components"
import React, { PropsWithChildren, useCallback } from "react"
import React, { PropsWithChildren, useCallback, useEffect } from "react"
import EmbedBtn from "./EmbedBtn"
import ImageEditModal from "./ImageEditModal"
@@ -8,28 +8,46 @@ import type {
IRteElementType,
IRteParam,
} from "@contentstack/app-sdk/dist/src/RTE/types"
import type { InsertResponse } from "~/types/imagevault"
import type { ImageVaultAsset, InsertResponse } from "~/types/imagevault"
import {
getImageVaultAssetFromData,
isInsertResponse,
} from "~/utils/imagevault"
type ImageElementProps = PropsWithChildren & {
element: IRteElementType & { attrs: InsertResponse }
element: IRteElementType & { attrs: ImageVaultAsset | InsertResponse }
rte: IRteParam
}
export function ImageElement({ children, element, rte }: ImageElementProps) {
const assetUrl = element.attrs.MediaConversions[0].Url
const isSelected = rte?.selection?.isSelected()
const isFocused = rte?.selection?.isFocused()
const imageVaultAsset = getImageVaultAssetFromData(element.attrs)
const isSelected = rte.selection.isSelected()
const isFocused = rte.selection.isFocused()
const path = rte.getPath(element)
const isHighlight = isFocused && isSelected
const handleMedia = useCallback(
(asset: ImageVaultAsset) => {
rte._adv.Transforms.setNodes<IRteElementType>(
rte._adv.editor,
{
attrs: { ...asset },
},
{ at: path }
)
},
[rte, path]
)
const handleEdit = useCallback(() => {
cbModal({
// @ts-expect-error: Component is badly typed
component: (compProps) =>
ImageEditModal({
element,
rte,
path: rte.getPath(element),
...compProps,
}),
component: (compProps) => (
<ImageEditModal
element={element}
setData={handleMedia}
{...compProps}
/>
),
modalProps: {
size: "max",
style: {
@@ -42,7 +60,15 @@ export function ImageElement({ children, element, rte }: ImageElementProps) {
},
},
})
}, [element, rte])
}, [element, handleMedia])
// The existing data might still be in InsertResponse format if the user has not edited it yet.
// We'll convert it to ImageVaultAsset when the user edits the RTE.
useEffect(() => {
if (isInsertResponse(element.attrs) && imageVaultAsset) {
handleMedia(imageVaultAsset)
}
}, [element.attrs, imageVaultAsset, handleMedia])
const ToolTipButtons = () => {
return (
@@ -54,7 +80,7 @@ export function ImageElement({ children, element, rte }: ImageElementProps) {
<EmbedBtn
title="remove"
content={"Remove"}
onClick={() => rte?.removeNode(element)}
onClick={() => rte.removeNode(element)}
>
<Icon icon="Trash" />
</EmbedBtn>
@@ -62,78 +88,46 @@ export function ImageElement({ children, element, rte }: ImageElementProps) {
)
}
let alignmentStyles = {}
const marginAlignment: Record<string, { [key: string]: string }> = {
center: { margin: "auto" },
left: { marginRight: "auto" },
right: { marginLeft: "auto" },
}
if (typeof element.attrs.position === "string") {
alignmentStyles = marginAlignment[element.attrs.position]
if (!imageVaultAsset) {
return <>{children}</>
}
return (
<div
style={{
...alignmentStyles,
...element.attrs.style,
}}
<Tooltip
zIndex={909}
className="p-0"
style={{ marginBottom: "10px" }}
position="top"
variantType="light"
offset={[0, -15]}
content={<ToolTipButtons />}
>
<Tooltip
zIndex={909}
className="p-0"
style={{ marginBottom: "10px" }}
position="top"
variantType="light"
offset={[0, -15]}
content={<ToolTipButtons />}
>
<span
data-type="asset"
<span data-type="asset" contentEditable={false}>
<div
contentEditable={false}
style={element.attrs?.style}
style={{
width: "280px",
height: "auto",
...(isHighlight ? { border: "1px solid #6c5ce7" } : {}),
}}
>
<div
contentEditable={false}
style={{
width: "280px",
height: "auto",
...(isHighlight ? { border: "1px solid #6c5ce7" } : {}),
<img
src={imageVaultAsset.url}
onError={(event) => {
event.currentTarget.src = "https://placehold.co/600x400"
}}
>
<img
src={assetUrl}
onError={(event) => {
event.currentTarget.src = "https://placehold.co/600x400"
}}
style={{
width: "100%",
height: "auto",
aspectRatio: element.attrs.MediaConversions[0].AspectRatio,
borderRadius: "8px",
}}
alt={element.attrs.altText}
title={element.attrs?.Name}
/>
<div
style={{
position: "absolute",
bottom: 0,
right: 0,
background: "#fff",
color: "#000",
height: "25px",
padding: "3px",
borderRadius: "4px",
}}
>
<Icon icon="Embed" />
</div>
</div>
{children}
</span>
</Tooltip>
</div>
style={{
width: "100%",
height: "auto",
aspectRatio: imageVaultAsset.dimensions.aspectRatio,
borderRadius: "8px",
}}
alt={imageVaultAsset.meta.alt}
title={`Id: ${imageVaultAsset.imageVaultId} - ${imageVaultAsset.fileName}`}
/>
</div>
{children}
</span>
</Tooltip>
)
}

View File

@@ -1,11 +1,10 @@
import ContentstackSDK from "@contentstack/app-sdk"
import React from "react"
import { ImageElement } from "~/components/ImageElement"
import { Icon } from "@contentstack/venus-components"
import { openImageVault } from "~/utils/imagevault"
import { ImageElement } from "~/components/ImageElement"
import type {
ContentstackEmbeddedData,
ContentstackPluginDefinition,
@@ -121,10 +120,10 @@ function loadIV(plugin: ContentstackPluginDefinition) {
}
}
export default ContentstackSDK.init().then(async (sdk) => {
export default ContentstackSDK.init().then((sdk) => {
try {
const extensionObj = await sdk["location"]
const RTE = await extensionObj["RTEPlugin"]
const extensionObj = sdk["location"]
const RTE = extensionObj["RTEPlugin"]
if (!RTE) {
return
@@ -178,7 +177,7 @@ export default ContentstackSDK.init().then(async (sdk) => {
ImageVault.on("exec", async (rte) => {
if (rte) {
const savedSelection = rte?.selection?.get() ?? undefined
const savedSelection = rte.selection.get() ?? undefined
const appConfig = await rte.getConfig()
@@ -214,7 +213,6 @@ export default ContentstackSDK.init().then(async (sdk) => {
type: "ImageVault",
attrs: {
...result,
FocalPoint: result.FocalPoint || { x: 50, y: 50 },
},
uid: crypto.randomUUID(),
children: [{ text: "" }],