Files
contentstack-imagevault/remix/app/components/ImageVaultDAM.tsx

221 lines
5.8 KiB
TypeScript

import { useCallback, useEffect, useState } from "react"
import { flushSync } from "react-dom"
import {
AssetCardVertical,
Button,
FieldLabel,
cbModal,
} from "@contentstack/venus-components"
import {
getImageVaultAssetFromData,
isInsertResponse,
openImageVault,
} from "~/utils/imagevault"
import ImageEditModal from "./ImageEditModal"
import type UiLocation from "@contentstack/app-sdk/dist/src/uiLocation"
import type { CbModalProps } from "@contentstack/venus-components/build/components/Modal/Modal"
import type { ImageVaultAsset, InsertResponse } from "~/types/imagevault"
import type { Lang } from "~/types/lang"
import type {
EntryDataPublishDetails,
ImageVaultDAMConfig,
} from "~/utils/imagevault"
import FullSizeImage from "./FullSizeImage"
export type ImageVaultDAMProps = {
sdk: UiLocation
config: ImageVaultDAMConfig
initialData: ImageVaultAsset | InsertResponse | null
}
type DAMButtonProps = { onClick: () => void }
function DAMButton({ onClick }: DAMButtonProps) {
return (
<div style={{ padding: 2 }}>
<Button
onClick={onClick}
buttonType="link"
version="v2"
icon="v2-Assets"
iconAlignment="left"
size="small"
id="imagevaultpicker"
>
Choose a file
</Button>
</div>
)
}
type MediaProps = {
media: ImageVaultAsset
onDelete: () => void
onEdit: () => void
}
function Media({ media, onDelete, onEdit }: MediaProps) {
const { url, meta, dimensions } = media
const { width, height, aspectRatio } = dimensions
const assetUrl = media.url
const assetTitle = `Id: ${media.imageVaultId} - ${media.fileName}`
const alt = meta.alt || meta.caption || ""
const title = meta.caption || meta.alt || ""
return (
<div key={url} style={{ fontFamily: "Inter" }}>
<AssetCardVertical
assetType="image"
assetUrl={assetUrl}
title={assetTitle}
version="v2"
width={width}
height={height}
onDeleteClick={onDelete}
onEditClick={onEdit}
onFullScreenClick={() => {
const cbModalProps: CbModalProps = {
// @ts-expect-error: Component is badly typed
component: (props) => {
return (
<FullSizeImage
// eslint-disable-next-line react/prop-types
onClose={props.closeModal}
imageUrl={url}
alt={alt}
title={title}
aspectRatio={aspectRatio}
/>
)
},
modalProps: {
size: "max",
style: {
content: {
display: "grid",
width: "90dvw",
height: "90dvh",
maxHeight: "100%",
gridTemplateRows: "auto 1fr",
},
overlay: {},
},
},
}
cbModal(cbModalProps)
}}
/>
</div>
)
}
export default function ImageVaultDAM({
sdk,
config,
initialData,
}: ImageVaultDAMProps) {
const imageVaultAsset = getImageVaultAssetFromData(initialData)
const [media, setMedia] = useState(imageVaultAsset)
const field = sdk.location.CustomField?.field
const frame = sdk.location.CustomField?.frame
const entry = sdk.location.CustomField?.entry
const stack = sdk.location.CustomField?.stack
const updateFrameHeight = useCallback(() => {
if (frame?.updateHeight) {
// We need to recalculate the height of the iframe when an image is added.
// Cannot use flushSync inside useEffect.
setTimeout(() => frame.updateHeight(), 0)
}
}, [frame])
const handleMedia = useCallback(
(asset?: ImageVaultAsset) => {
if (field) {
flushSync(() => {
const data = asset || null
setMedia(data)
field.setData(data)
document.body.style.overflow = "hidden"
})
}
updateFrameHeight()
},
[field, updateFrameHeight]
)
const handleEdit = useCallback(() => {
const fieldData = field?.getData()
if (fieldData) {
cbModal({
// @ts-expect-error: Component is badly typed
component: (compProps) => (
<ImageEditModal
fieldData={fieldData}
setData={handleMedia}
{...compProps}
/>
),
modalProps: {
size: "max",
style: {
content: { maxHeight: "90dvh", maxWidth: "90dvw", width: "auto" },
overlay: {},
},
},
})
}
}, [field, handleMedia])
useEffect(() => {
updateFrameHeight()
}, [updateFrameHeight])
// 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 the emtry.
useEffect(() => {
if (isInsertResponse(initialData) && imageVaultAsset) {
handleMedia(imageVaultAsset)
}
}, [initialData, imageVaultAsset, handleMedia])
if (!field || !frame || !entry || !stack) {
return <p>Initializing custom field...</p>
}
const entryData: EntryDataPublishDetails = {
//TODO: Add support for branches
branch: "main",
contentTypeUid: entry.content_type.uid,
locale: entry.locale as Lang,
stackApiKey: stack._data.api_key,
title:
entry.getField("title").getData().toString() ||
`Untitled (${entry.content_type.uid})`,
uid: entry._data.uid,
}
return (
<div>
<FieldLabel htmlFor="imagevaultpicker" version="v2">
ImageVault Asset
</FieldLabel>
{media ? (
<Media media={media} onDelete={handleMedia} onEdit={handleEdit} />
) : (
<DAMButton
onClick={() => {
openImageVault({ config, entryData, onSuccess: handleMedia })
}}
/>
)}
</div>
)
}