Compare commits
No commits in common. "95e02dcd6342d7e4601271e66002224bc05d212e" and "36a3e81f47027bc8c952155753ba6ef2d7e49054" have entirely different histories.
95e02dcd63
...
36a3e81f47
@ -792,10 +792,6 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-descriptions-small .ant-descriptions-row > td {
|
|
||||||
padding-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimal-code-block-editor .ant-typography pre {
|
.minimal-code-block-editor .ant-typography pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
|||||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
import HistoryDisplay from '../../common/HistoryDisplay.jsx'
|
import HistoryDisplay from '../../common/HistoryDisplay.jsx'
|
||||||
import PartStockIcon from '../../../Icons/PartStockIcon.jsx'
|
|
||||||
|
|
||||||
const log = loglevel.getLogger('PartStockInfo')
|
const log = loglevel.getLogger('PartStockInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -38,7 +37,7 @@ const PartStockInfo = () => {
|
|||||||
'PartStockInfo',
|
'PartStockInfo',
|
||||||
{
|
{
|
||||||
info: true,
|
info: true,
|
||||||
events: true,
|
stocks: true,
|
||||||
history: true,
|
history: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
auditLogs: false
|
auditLogs: false
|
||||||
@ -92,7 +91,6 @@ const PartStockInfo = () => {
|
|||||||
disabled={objectFormState.loading}
|
disabled={objectFormState.loading}
|
||||||
items={[
|
items={[
|
||||||
{ key: 'info', label: 'Part Stock Information' },
|
{ key: 'info', label: 'Part Stock Information' },
|
||||||
{ key: 'events', label: 'Part Stock Events' },
|
|
||||||
{ key: 'history', label: 'Part Stock History' },
|
{ key: 'history', label: 'Part Stock History' },
|
||||||
{ key: 'notes', label: 'Notes' },
|
{ key: 'notes', label: 'Notes' },
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
@ -130,9 +128,7 @@ const PartStockInfo = () => {
|
|||||||
}}
|
}}
|
||||||
editLoading={objectFormState.editLoading}
|
editLoading={objectFormState.editLoading}
|
||||||
formValid={objectFormState.formValid}
|
formValid={objectFormState.formValid}
|
||||||
disabled={
|
disabled={objectFormState.beingEditedByOther || objectFormState.loading}
|
||||||
objectFormState.beingEditedByOther || objectFormState.loading
|
|
||||||
}
|
|
||||||
loading={objectFormState.editLoading}
|
loading={objectFormState.editLoading}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
@ -181,24 +177,6 @@ const PartStockInfo = () => {
|
|||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</ActionHandler>
|
</ActionHandler>
|
||||||
|
|
||||||
<InfoCollapse
|
|
||||||
title='Part Stock Events'
|
|
||||||
icon={<PartStockIcon />}
|
|
||||||
active={collapseState.events}
|
|
||||||
onToggle={(expanded) => updateCollapseState('events', expanded)}
|
|
||||||
collapseKey='events'
|
|
||||||
>
|
|
||||||
{objectFormState.loading ? (
|
|
||||||
<InfoCollapsePlaceholder />
|
|
||||||
) : (
|
|
||||||
<ObjectTable
|
|
||||||
type='stockEvent'
|
|
||||||
masterFilter={{ 'parent._id': partStockId }}
|
|
||||||
visibleColumns={{ parent: false }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Part Stock History'
|
title='Part Stock History'
|
||||||
icon={<GraphIcon />}
|
icon={<GraphIcon />}
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
|||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import PartStockIcon from '../../../Icons/PartStockIcon.jsx'
|
import PartStockIcon from '../../../Icons/PartStockIcon.jsx'
|
||||||
import ProductStockIcon from '../../../Icons/ProductStockIcon.jsx'
|
|
||||||
import GraphIcon from '../../../Icons/GraphIcon.jsx'
|
import GraphIcon from '../../../Icons/GraphIcon.jsx'
|
||||||
import ObjectForm from '../../common/ObjectForm.jsx'
|
import ObjectForm from '../../common/ObjectForm.jsx'
|
||||||
import EditButtons from '../../common/EditButtons.jsx'
|
import EditButtons from '../../common/EditButtons.jsx'
|
||||||
@ -49,7 +48,6 @@ const ProductStockInfo = () => {
|
|||||||
{
|
{
|
||||||
info: true,
|
info: true,
|
||||||
partStocks: true,
|
partStocks: true,
|
||||||
events: true,
|
|
||||||
history: true,
|
history: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
auditLogs: false
|
auditLogs: false
|
||||||
@ -117,7 +115,6 @@ const ProductStockInfo = () => {
|
|||||||
items={[
|
items={[
|
||||||
{ key: 'info', label: 'Product Stock Information' },
|
{ key: 'info', label: 'Product Stock Information' },
|
||||||
{ key: 'partStocks', label: 'Part Stocks' },
|
{ key: 'partStocks', label: 'Part Stocks' },
|
||||||
{ key: 'events', label: 'Product Stock Events' },
|
|
||||||
{ key: 'history', label: 'Product Stock History' },
|
{ key: 'history', label: 'Product Stock History' },
|
||||||
{ key: 'notes', label: 'Notes' },
|
{ key: 'notes', label: 'Notes' },
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
@ -224,24 +221,6 @@ const ProductStockInfo = () => {
|
|||||||
</ObjectForm>
|
</ObjectForm>
|
||||||
</ActionHandler>
|
</ActionHandler>
|
||||||
|
|
||||||
<InfoCollapse
|
|
||||||
title='Product Stock Events'
|
|
||||||
icon={<ProductStockIcon />}
|
|
||||||
active={collapseState.events}
|
|
||||||
onToggle={(expanded) => updateCollapseState('events', expanded)}
|
|
||||||
collapseKey='events'
|
|
||||||
>
|
|
||||||
{objectFormState.loading ? (
|
|
||||||
<InfoCollapsePlaceholder />
|
|
||||||
) : (
|
|
||||||
<ObjectTable
|
|
||||||
type='stockEvent'
|
|
||||||
masterFilter={{ 'parent._id': productStockId }}
|
|
||||||
visibleColumns={{ parent: false }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InfoCollapse>
|
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Product Stock History'
|
title='Product Stock History'
|
||||||
icon={<GraphIcon />}
|
icon={<GraphIcon />}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { useContext, memo } from 'react'
|
import { useState, useEffect, useContext, useRef, memo } from 'react'
|
||||||
import { Flex, Popover, Divider } from 'antd'
|
import { Flex, Card, Popover, Divider } from 'antd'
|
||||||
import { UserOutlined } from '@ant-design/icons'
|
import { UserOutlined } from '@ant-design/icons'
|
||||||
|
import { decode } from 'blurhash'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import EditIcon from '../../Icons/EditIcon'
|
import EditIcon from '../../Icons/EditIcon'
|
||||||
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import SpotlightTooltip from './SpotlightTooltip'
|
import SpotlightTooltip from './SpotlightTooltip'
|
||||||
import Thumbnail from './Thumbnail'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ const getDisplayName = (user) =>
|
|||||||
user?.username ||
|
user?.username ||
|
||||||
'User'
|
'User'
|
||||||
|
|
||||||
const avatarStyle = {
|
const avatarImageStyle = {
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
@ -41,30 +42,132 @@ const avatarStyle = {
|
|||||||
border: '1px solid rgb(128, 128, 128, 0.4)'
|
border: '1px solid rgb(128, 128, 128, 0.4)'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const avatarCardStyle = { borderRadius: '12px', width: 32, height: 32 }
|
||||||
|
|
||||||
const UserProfileImage = memo(function UserProfileImage({
|
const UserProfileImage = memo(function UserProfileImage({
|
||||||
profileImageId,
|
profileImageId,
|
||||||
displayName,
|
displayName,
|
||||||
blurHash
|
blurHash
|
||||||
}) {
|
}) {
|
||||||
const file = profileImageId
|
const { fetchFileThumbnail } = useContext(ApiServerContext)
|
||||||
? { _id: profileImageId, metaData: blurHash ? { blurHash } : undefined }
|
const fetchFileThumbnailRef = useRef(fetchFileThumbnail)
|
||||||
: null
|
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 (
|
||||||
|
<img src={profileImageUrl} alt={displayName} style={avatarImageStyle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileImageId && blurHashUrl) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={blurHashUrl}
|
||||||
|
alt=''
|
||||||
|
aria-hidden
|
||||||
|
style={avatarImageStyle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Thumbnail
|
<Card
|
||||||
file={file}
|
style={avatarCardStyle}
|
||||||
size={64}
|
styles={{ body: { padding: 0, height: '100%' } }}
|
||||||
alt={displayName}
|
>
|
||||||
style={avatarStyle}
|
<Flex justify='center' align='center' style={{ height: '100%' }}>
|
||||||
fallback={
|
|
||||||
<UserOutlined
|
<UserOutlined
|
||||||
style={{
|
style={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: 'var(--color-text-secondary)'
|
color: 'var(--color-text-secondary)'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
</Flex>
|
||||||
/>
|
</Card>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { Descriptions, Card, Flex, Divider } from 'antd'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import { createElement } from 'react'
|
import { createElement } from 'react'
|
||||||
import Thumbnail from './Thumbnail'
|
|
||||||
|
|
||||||
const ObjectCard = ({
|
const ObjectCard = ({
|
||||||
model,
|
model,
|
||||||
@ -17,12 +16,6 @@ const ObjectCard = ({
|
|||||||
const descriptionItems = []
|
const descriptionItems = []
|
||||||
const modelIcon = createElement(model.icon, { style: { fontSize: 24 } })
|
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) => {
|
model.columns.forEach((colName) => {
|
||||||
const prop = modelProperties.find((p) => p.name === colName)
|
const prop = modelProperties.find((p) => p.name === colName)
|
||||||
if (prop) {
|
if (prop) {
|
||||||
@ -30,17 +23,13 @@ const ObjectCard = ({
|
|||||||
(Object.keys(visibleColumns).length > 0 &&
|
(Object.keys(visibleColumns).length > 0 &&
|
||||||
visibleColumns[prop.name] === false) ||
|
visibleColumns[prop.name] === false) ||
|
||||||
prop.name == 'name' ||
|
prop.name == 'name' ||
|
||||||
(prop.name == 'state' && visibleColumns?.name == true) ||
|
(prop.name == 'state' && visibleColumns?.name == true)
|
||||||
(thumbnailPresent && prop.name === thumbnailProperty.name)
|
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptionItems.push({
|
descriptionItems.push(
|
||||||
key: prop.name,
|
<Descriptions.Item label={prop.label} key={prop.name} colspan={2}>
|
||||||
label: prop.label,
|
|
||||||
span: 2,
|
|
||||||
children: (
|
|
||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
{...prop}
|
{...prop}
|
||||||
longId={false}
|
longId={false}
|
||||||
@ -48,8 +37,8 @@ const ObjectCard = ({
|
|||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
name={prop.name}
|
name={prop.name}
|
||||||
/>
|
/>
|
||||||
)
|
</Descriptions.Item>
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -59,91 +48,50 @@ const ObjectCard = ({
|
|||||||
actions = renderActions(record)
|
actions = renderActions(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryDescriptionItems = thumbnailPresent
|
|
||||||
? descriptionItems.slice(0, 2)
|
|
||||||
: []
|
|
||||||
const remainingDescriptionItems = thumbnailPresent
|
|
||||||
? descriptionItems.slice(2)
|
|
||||||
: descriptionItems
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
styles={{ body: { padding: 18 } }}
|
styles={{ body: { padding: 18 } }}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
variant={cardStyle}
|
variant={cardStyle}
|
||||||
>
|
>
|
||||||
<Flex gap={6} vertical>
|
<Flex vertical gap={8}>
|
||||||
{thumbnailPresent && (
|
{visibleColumns?.name == true && (
|
||||||
<Flex gap={14}>
|
<Flex align='center' gap={12}>
|
||||||
<Thumbnail
|
{modelIcon}
|
||||||
file={{
|
<ObjectProperty
|
||||||
_id: record?.[thumbnailProperty.name]?._id,
|
{...model.properties.find((p) => p.name === 'name')}
|
||||||
type: 'image'
|
objectData={record}
|
||||||
}}
|
isEditing={isEditing}
|
||||||
style={{
|
style={{
|
||||||
width: 84,
|
fontSize: 20,
|
||||||
height: 84,
|
fontWeight: '600',
|
||||||
marginBottom: 4
|
lineHeight: 1.2
|
||||||
}}
|
}}
|
||||||
size={128}
|
|
||||||
/>
|
/>
|
||||||
<Flex vertical gap={10} style={{ minWidth: 0 }}>
|
{visibleColumns?.state == true && (
|
||||||
{visibleColumns?.name == true && (
|
<ObjectProperty
|
||||||
<Flex align='center' gap={12} style={{ minWidth: 0 }}>
|
{...model.properties.find((p) => p.name === 'state')}
|
||||||
{modelIcon}
|
objectData={record}
|
||||||
<ObjectProperty
|
/>
|
||||||
{...model.properties.find((p) => p.name === 'name')}
|
)}
|
||||||
objectData={record}
|
|
||||||
isEditing={isEditing}
|
|
||||||
style={{
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '600',
|
|
||||||
lineHeight: 1.2,
|
|
||||||
minWidth: 0
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{visibleColumns?.state == true && (
|
|
||||||
<ObjectProperty
|
|
||||||
{...model.properties.find((p) => p.name === 'state')}
|
|
||||||
objectData={record}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
{primaryDescriptionItems.length > 0 && (
|
|
||||||
<Descriptions
|
|
||||||
column={1}
|
|
||||||
size='small'
|
|
||||||
style={{ width: '100%', tableLayout: 'fixed' }}
|
|
||||||
className='objectTableDescritions'
|
|
||||||
items={primaryDescriptionItems}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
<Flex vertical gap={8}>
|
<Descriptions
|
||||||
{remainingDescriptionItems.length > 0 && (
|
column={1}
|
||||||
<Descriptions
|
size='small'
|
||||||
column={1}
|
style={{ width: '100%', tableLayout: 'fixed' }}
|
||||||
size='small'
|
className='objectTableDescritions'
|
||||||
style={{
|
>
|
||||||
width: '100%',
|
{descriptionItems}
|
||||||
tableLayout: 'fixed'
|
</Descriptions>
|
||||||
}}
|
{actions && (
|
||||||
className='objectTableDescritions'
|
<>
|
||||||
items={remainingDescriptionItems}
|
<Divider style={{ margin: '2px 0 0 0' }} />
|
||||||
/>
|
<Flex align='flex-end' gap={10}>
|
||||||
)}
|
{actions}
|
||||||
{actions && (
|
</Flex>
|
||||||
<>
|
</>
|
||||||
<Divider style={{ margin: '4px 0 0 0' }} />
|
)}
|
||||||
<Flex align='flex-end' gap={10}>
|
|
||||||
{actions}
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1086,12 +1086,11 @@ const ObjectTable = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
xs={24}
|
xs={24}
|
||||||
sm={24}
|
sm={12}
|
||||||
md={24}
|
md={12}
|
||||||
lg={12}
|
lg={8}
|
||||||
xl={12}
|
xl={6}
|
||||||
xxl={8}
|
xxl={6}
|
||||||
xxxl={6}
|
|
||||||
key={record._id}
|
key={record._id}
|
||||||
>
|
>
|
||||||
<div style={{ width: '100%', overflow: 'hidden' }}>
|
<div style={{ width: '100%', overflow: 'hidden' }}>
|
||||||
|
|||||||
@ -1,189 +0,0 @@
|
|||||||
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 (
|
|
||||||
<img
|
|
||||||
src={thumbnailUrl}
|
|
||||||
alt={alt}
|
|
||||||
className={className}
|
|
||||||
style={imageStyle}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileId && blurHashUrl) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={blurHashUrl}
|
|
||||||
alt=''
|
|
||||||
aria-hidden
|
|
||||||
className={className}
|
|
||||||
style={imageStyle}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={className}
|
|
||||||
style={cardStyle}
|
|
||||||
styles={{ body: { padding: 0, height: '100%' } }}
|
|
||||||
>
|
|
||||||
<Flex justify='center' align='center' style={{ height: '100%' }}>
|
|
||||||
{fallback || (
|
|
||||||
<FileOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: Math.round(size * 0.22),
|
|
||||||
color: 'var(--color-text-secondary)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
@ -116,7 +116,6 @@ export const User = {
|
|||||||
fileType: 'image',
|
fileType: 'image',
|
||||||
previewOpen: true,
|
previewOpen: true,
|
||||||
showPreview: false,
|
showPreview: false,
|
||||||
thumbnail: true,
|
|
||||||
columnWidth: 150,
|
columnWidth: 150,
|
||||||
masterFilter: ['.png', '.jpg', '.jpeg']
|
masterFilter: ['.png', '.jpg', '.jpeg']
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user