diff --git a/src/components/Dashboard/common/ActivityIndicator.jsx b/src/components/Dashboard/common/ActivityIndicator.jsx
index fcd1453..c60ba59 100644
--- a/src/components/Dashboard/common/ActivityIndicator.jsx
+++ b/src/components/Dashboard/common/ActivityIndicator.jsx
@@ -1,12 +1,11 @@
-import { useState, useEffect, useContext, useRef, memo } from 'react'
-import { Flex, Card, Popover, Divider } from 'antd'
+import { useContext, memo } from 'react'
+import { Flex, Popover, Divider } from 'antd'
import { UserOutlined } from '@ant-design/icons'
-import { decode } from 'blurhash'
import PropTypes from 'prop-types'
import EditIcon from '../../Icons/EditIcon'
-import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import SpotlightTooltip from './SpotlightTooltip'
+import Thumbnail from './Thumbnail'
import { useNavigate } from 'react-router-dom'
import { getModelByName } from '../../../database/ObjectModels'
@@ -34,7 +33,7 @@ const getDisplayName = (user) =>
user?.username ||
'User'
-const avatarImageStyle = {
+const avatarStyle = {
width: 32,
height: 32,
borderRadius: '12px',
@@ -42,132 +41,30 @@ const avatarImageStyle = {
border: '1px solid rgb(128, 128, 128, 0.4)'
}
-const avatarCardStyle = { borderRadius: '12px', width: 32, height: 32 }
-
const UserProfileImage = memo(function UserProfileImage({
profileImageId,
displayName,
blurHash
}) {
- const { fetchFileThumbnail } = useContext(ApiServerContext)
- const fetchFileThumbnailRef = useRef(fetchFileThumbnail)
- fetchFileThumbnailRef.current = fetchFileThumbnail
-
- const [profileImageUrl, setProfileImageUrl] = useState(null)
- const [blurHashUrl, setBlurHashUrl] = useState(null)
- const profileImageUrlRef = useRef(null)
- const loadedImageIdRef = useRef(null)
-
- useEffect(() => {
- if (!blurHash) {
- setBlurHashUrl(null)
- return
- }
-
- try {
- const pixels = decode(blurHash, 32, 32)
- const canvas = document.createElement('canvas')
- canvas.width = 32
- canvas.height = 32
- const ctx = canvas.getContext('2d')
- const imageData = ctx.createImageData(32, 32)
- imageData.data.set(pixels)
- ctx.putImageData(imageData, 0, 0)
- setBlurHashUrl(canvas.toDataURL())
- } catch {
- setBlurHashUrl(null)
- }
- }, [blurHash])
-
- useEffect(() => {
- if (!profileImageId) {
- if (profileImageUrlRef.current) {
- URL.revokeObjectURL(profileImageUrlRef.current)
- profileImageUrlRef.current = null
- }
- loadedImageIdRef.current = null
- setProfileImageUrl(null)
- return
- }
-
- if (
- loadedImageIdRef.current === profileImageId &&
- profileImageUrlRef.current
- ) {
- return
- }
-
- let cancelled = false
- const file = { _id: profileImageId }
-
- const loadProfileImage = async () => {
- try {
- const fileURL = await fetchFileThumbnailRef.current(file, 64)
- if (!cancelled) {
- if (profileImageUrlRef.current) {
- URL.revokeObjectURL(profileImageUrlRef.current)
- }
- profileImageUrlRef.current = fileURL || null
- loadedImageIdRef.current = profileImageId
- setProfileImageUrl(fileURL || null)
- } else if (fileURL) {
- URL.revokeObjectURL(fileURL)
- }
- } catch {
- if (!cancelled) {
- setProfileImageUrl(null)
- }
- }
- }
-
- loadProfileImage()
-
- return () => {
- cancelled = true
- }
- }, [profileImageId])
-
- useEffect(() => {
- return () => {
- if (profileImageUrlRef.current) {
- URL.revokeObjectURL(profileImageUrlRef.current)
- profileImageUrlRef.current = null
- }
- loadedImageIdRef.current = null
- }
- }, [])
-
- if (profileImageUrl) {
- return (
-
- )
- }
-
- if (profileImageId && blurHashUrl) {
- return (
-
- )
- }
+ const file = profileImageId
+ ? { _id: profileImageId, metaData: blurHash ? { blurHash } : undefined }
+ : null
return (
-
-
+
-
-
+ }
+ />
)
})
diff --git a/src/components/Dashboard/common/ObjectCard.jsx b/src/components/Dashboard/common/ObjectCard.jsx
index ef47e75..35af1ef 100644
--- a/src/components/Dashboard/common/ObjectCard.jsx
+++ b/src/components/Dashboard/common/ObjectCard.jsx
@@ -2,6 +2,7 @@ import { Descriptions, Card, Flex, Divider } from 'antd'
import PropTypes from 'prop-types'
import ObjectProperty from './ObjectProperty'
import { createElement } from 'react'
+import Thumbnail from './Thumbnail'
const ObjectCard = ({
model,
@@ -16,6 +17,12 @@ const ObjectCard = ({
const descriptionItems = []
const modelIcon = createElement(model.icon, { style: { fontSize: 24 } })
+ const thumbnailProperty =
+ model.properties.find((p) => p.thumbnail === true) || null
+
+ const thumbnailPresent =
+ thumbnailProperty !== null && record?.[thumbnailProperty.name]?._id
+
model.columns.forEach((colName) => {
const prop = modelProperties.find((p) => p.name === colName)
if (prop) {
@@ -23,13 +30,17 @@ const ObjectCard = ({
(Object.keys(visibleColumns).length > 0 &&
visibleColumns[prop.name] === false) ||
prop.name == 'name' ||
- (prop.name == 'state' && visibleColumns?.name == true)
+ (prop.name == 'state' && visibleColumns?.name == true) ||
+ (thumbnailPresent && prop.name === thumbnailProperty.name)
) {
return
}
- descriptionItems.push(
-
+ descriptionItems.push({
+ key: prop.name,
+ label: prop.label,
+ span: 2,
+ children: (
-
- )
+ )
+ })
}
})
@@ -48,50 +59,91 @@ const ObjectCard = ({
actions = renderActions(record)
}
+ const primaryDescriptionItems = thumbnailPresent
+ ? descriptionItems.slice(0, 2)
+ : []
+ const remainingDescriptionItems = thumbnailPresent
+ ? descriptionItems.slice(2)
+ : descriptionItems
+
return (
-
- {visibleColumns?.name == true && (
-
- {modelIcon}
- p.name === 'name')}
- objectData={record}
- isEditing={isEditing}
- style={{
- fontSize: 20,
- fontWeight: '600',
- lineHeight: 1.2
+
+ {thumbnailPresent && (
+
+
- {visibleColumns?.state == true && (
- p.name === 'state')}
- objectData={record}
- />
- )}
+
+ {visibleColumns?.name == true && (
+
+ {modelIcon}
+ p.name === 'name')}
+ objectData={record}
+ isEditing={isEditing}
+ style={{
+ fontSize: 20,
+ fontWeight: '600',
+ lineHeight: 1.2,
+ minWidth: 0
+ }}
+ />
+ {visibleColumns?.state == true && (
+ p.name === 'state')}
+ objectData={record}
+ />
+ )}
+
+ )}
+ {primaryDescriptionItems.length > 0 && (
+
+ )}
+
)}
-
- {descriptionItems}
-
- {actions && (
- <>
-
-
- {actions}
-
- >
- )}
+
+ {remainingDescriptionItems.length > 0 && (
+
+ )}
+ {actions && (
+ <>
+
+
+ {actions}
+
+ >
+ )}
+
)
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index e931a7c..7798f93 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -1086,11 +1086,12 @@ const ObjectTable = forwardRef(
return (
diff --git a/src/components/Dashboard/common/Thumbnail.jsx b/src/components/Dashboard/common/Thumbnail.jsx
new file mode 100644
index 0000000..b2f34b5
--- /dev/null
+++ b/src/components/Dashboard/common/Thumbnail.jsx
@@ -0,0 +1,189 @@
+import { useState, useEffect, useContext, useRef, memo } from 'react'
+import { Flex, Card } from 'antd'
+import { FileOutlined } from '@ant-design/icons'
+import { decode } from 'blurhash'
+import PropTypes from 'prop-types'
+import { ApiServerContext } from '../context/ApiServerContext'
+
+const getFileId = (file) =>
+ file?._id || (typeof file === 'string' ? file : null)
+
+const getBlurHash = (file) => file?.metaData?.blurHash || null
+
+const Thumbnail = function Thumbnail({
+ file,
+ size = 64,
+ alt = '',
+ fallback = null,
+ style = {},
+ className
+}) {
+ const { fetchFileThumbnail } = useContext(ApiServerContext)
+ const fetchFileThumbnailRef = useRef(fetchFileThumbnail)
+ fetchFileThumbnailRef.current = fetchFileThumbnail
+
+ const fileId = getFileId(file)
+ const blurHash = getBlurHash(file)
+
+ const [thumbnailUrl, setThumbnailUrl] = useState(null)
+ const [blurHashUrl, setBlurHashUrl] = useState(null)
+ const thumbnailUrlRef = useRef(null)
+ const loadedKeyRef = useRef(null)
+
+ const imageStyle = {
+ width: size,
+ height: size,
+ borderRadius: '5px',
+ objectFit: 'cover',
+ ...style
+ }
+
+ const cardStyle = {
+ borderRadius: '5px',
+ width: size,
+ height: size,
+ border: 'none',
+ ...style
+ }
+
+ useEffect(() => {
+ if (!blurHash) {
+ setBlurHashUrl(null)
+ return
+ }
+
+ try {
+ const pixels = decode(blurHash, 32, 32)
+ const canvas = document.createElement('canvas')
+ canvas.width = 32
+ canvas.height = 32
+ const ctx = canvas.getContext('2d')
+ const imageData = ctx.createImageData(32, 32)
+ imageData.data.set(pixels)
+ ctx.putImageData(imageData, 0, 0)
+ setBlurHashUrl(canvas.toDataURL())
+ } catch {
+ setBlurHashUrl(null)
+ }
+ }, [blurHash])
+
+ useEffect(() => {
+ if (!fileId) {
+ if (thumbnailUrlRef.current) {
+ URL.revokeObjectURL(thumbnailUrlRef.current)
+ thumbnailUrlRef.current = null
+ }
+ loadedKeyRef.current = null
+ setThumbnailUrl(null)
+ return
+ }
+
+ const loadKey = `${fileId}-${size}`
+ if (loadedKeyRef.current === loadKey && thumbnailUrlRef.current) {
+ return
+ }
+
+ let cancelled = false
+ const fileObject =
+ typeof file === 'object' && file !== null ? file : { _id: fileId }
+
+ const loadThumbnail = async () => {
+ try {
+ const fileURL = await fetchFileThumbnailRef.current(fileObject, size)
+ if (!cancelled) {
+ if (thumbnailUrlRef.current) {
+ URL.revokeObjectURL(thumbnailUrlRef.current)
+ }
+ thumbnailUrlRef.current = fileURL || null
+ loadedKeyRef.current = loadKey
+ setThumbnailUrl(fileURL || null)
+ } else if (fileURL) {
+ URL.revokeObjectURL(fileURL)
+ }
+ } catch {
+ if (!cancelled) {
+ setThumbnailUrl(null)
+ }
+ }
+ }
+
+ loadThumbnail()
+
+ return () => {
+ cancelled = true
+ }
+ }, [file, fileId, size])
+
+ useEffect(() => {
+ return () => {
+ if (thumbnailUrlRef.current) {
+ URL.revokeObjectURL(thumbnailUrlRef.current)
+ thumbnailUrlRef.current = null
+ }
+ loadedKeyRef.current = null
+ }
+ }, [])
+
+ if (thumbnailUrl) {
+ return (
+

+ )
+ }
+
+ if (fileId && blurHashUrl) {
+ return (
+

+ )
+ }
+
+ return (
+
+
+ {fallback || (
+
+ )}
+
+
+ )
+}
+
+Thumbnail.propTypes = {
+ file: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
+ size: PropTypes.oneOf([64, 128, 256]),
+ alt: PropTypes.string,
+ fallback: PropTypes.node,
+ style: PropTypes.object,
+ className: PropTypes.string
+}
+
+const areEqual = (prevProps, nextProps) => {
+ return (
+ getFileId(prevProps.file) === getFileId(nextProps.file) &&
+ getBlurHash(prevProps.file) === getBlurHash(nextProps.file) &&
+ prevProps.size === nextProps.size &&
+ prevProps.alt === nextProps.alt &&
+ JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style)
+ )
+}
+
+export default memo(Thumbnail, areEqual)