Refactor ObjectCard and ObjectTable components for improved layout and responsiveness
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced a new method to dynamically calculate card column span based on container width in ObjectTable, enhancing responsiveness.
- Updated ObjectCard to simplify rendering logic by extracting name and state properties into a separate component.
- Added min-width: 0 to relevant CSS classes in App.css to ensure proper layout handling and prevent overflow issues.
This commit is contained in:
Tom Butcher 2026-08-01 00:05:10 +01:00
parent c6ad465ad6
commit 3c634a90d0
4 changed files with 128 additions and 54 deletions

View File

@ -811,6 +811,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel { .farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel {
width: 100%; width: 100%;
min-width: 0;
} }
.farmcontrol-splitter .farmcontrol-splitter
@ -829,6 +830,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.objectTableCardsContainer { .objectTableCardsContainer {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-width: 0;
} }
.objectTableCardsContainer .ant-spin-nested-loading { .objectTableCardsContainer .ant-spin-nested-loading {

View File

@ -66,6 +66,31 @@ const ObjectCard = ({
? descriptionItems.slice(2) ? descriptionItems.slice(2)
: descriptionItems : descriptionItems
const nameContent = (
<>
<Flex align='center' gap={12} style={{ minWidth: 0 }}>
{modelIcon}
<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>
</>
)
return ( return (
<Card <Card
styles={{ body: { padding: 18 } }} styles={{ body: { padding: 18 } }}
@ -88,28 +113,7 @@ const ObjectCard = ({
size={128} size={128}
/> />
<Flex vertical gap={10} style={{ minWidth: 0 }}> <Flex vertical gap={10} style={{ minWidth: 0 }}>
{visibleColumns?.name == true && ( {visibleColumns?.name == true && nameContent}
<Flex align='center' gap={12} style={{ minWidth: 0 }}>
{modelIcon}
<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 && ( {primaryDescriptionItems.length > 0 && (
<Descriptions <Descriptions
column={1} column={1}
@ -123,6 +127,7 @@ const ObjectCard = ({
</Flex> </Flex>
)} )}
<Flex vertical gap={8}> <Flex vertical gap={8}>
{visibleColumns?.name == true && !thumbnailPresent && nameContent}
{remainingDescriptionItems.length > 0 && ( {remainingDescriptionItems.length > 0 && (
<Descriptions <Descriptions
column={1} column={1}

View File

@ -53,6 +53,14 @@ logger.setLevel(config.logLevel)
const SCROLL_THRESHOLD = 50 const SCROLL_THRESHOLD = 50
const SKELETON_HEIGHT = 49.5 const SKELETON_HEIGHT = 49.5
const getCardColSpan = (containerWidth) => {
if (containerWidth >= 2980) return 2
if (containerWidth >= 1780) return 4
if (containerWidth >= 1080) return 8
if (containerWidth >= 680) return 12
return 24
}
const RowForm = ({ record, isEditing, onRegister, children }) => { const RowForm = ({ record, isEditing, onRegister, children }) => {
const [form] = Form.useForm() const [form] = Form.useForm()
useEffect(() => { useEffect(() => {
@ -1038,13 +1046,68 @@ const ObjectTable = forwardRef(
} }
// Card view rendering // Card view rendering
const cardsContainerRef = useRef(null) const [cardColSpan, setCardColSpan] = useState(24)
const cardsContainerNodeRef = useRef(null)
const cardsResizeObserverRef = useRef(null)
const measureCardColSpan = useCallback(() => {
const node = cardsContainerNodeRef.current
if (!node) return
const panel = node.closest('.ant-splitter-panel')
const width = (panel ?? node).getBoundingClientRect().width
if (width <= 0) return
const nextSpan = getCardColSpan(width)
setCardColSpan((prev) => (prev === nextSpan ? prev : nextSpan))
}, [])
const objectTableCardsContainerRef = useCallback(
(node) => {
cardsResizeObserverRef.current?.disconnect()
cardsResizeObserverRef.current = null
cardsContainerNodeRef.current = node
if (!node) return
let rafId = null
const scheduleMeasure = () => {
if (rafId != null) cancelAnimationFrame(rafId)
rafId = requestAnimationFrame(() => {
rafId = null
measureCardColSpan()
})
}
scheduleMeasure()
const observer = new ResizeObserver(scheduleMeasure)
const panel = node.closest('.ant-splitter-panel')
if (panel) {
observer.observe(panel)
} else {
observer.observe(node)
}
cardsResizeObserverRef.current = observer
},
[measureCardColSpan]
)
useLayoutEffect(() => {
if (!cards) return
measureCardColSpan()
}, [cards, showFilterSidebar, measureCardColSpan])
useEffect(() => {
return () => cardsResizeObserverRef.current?.disconnect()
}, [])
// Card view scroll handler // Card view scroll handler
useEffect(() => { useEffect(() => {
if (!cards) return if (!cards) return
const container = cardsContainerRef.current const container = cardsContainerNodeRef.current
if (!container) return if (!container) return
const scrollEl = container.querySelector('.simplebar-content-wrapper')
if (!scrollEl) return
const handleCardsScroll = (e) => { const handleCardsScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target const { scrollTop, scrollHeight, clientHeight } = e.target
@ -1056,18 +1119,18 @@ const ObjectTable = forwardRef(
scrollHeight - scrollTop - clientHeight < 100 && scrollHeight - scrollTop - clientHeight < 100 &&
!lazyLoading !lazyLoading
) { ) {
loadNextPage() loadNextPage(scrollEl)
} else if ( } else if (
firstPage?.isSkeletonPage && firstPage?.isSkeletonPage &&
scrollTop < 100 && scrollTop < 100 &&
!lazyLoading !lazyLoading
) { ) {
loadPreviousPage() loadPreviousPage(scrollEl)
} }
} }
container.addEventListener('scroll', handleCardsScroll) scrollEl.addEventListener('scroll', handleCardsScroll)
return () => container.removeEventListener('scroll', handleCardsScroll) return () => scrollEl.removeEventListener('scroll', handleCardsScroll)
}, [cards, pages, lazyLoading, loadNextPage, loadPreviousPage]) }, [cards, pages, lazyLoading, loadNextPage, loadPreviousPage])
const renderCards = () => { const renderCards = () => {
@ -1078,22 +1141,13 @@ const ObjectTable = forwardRef(
}} }}
> >
<div className='objectTableCards'> <div className='objectTableCards'>
<Row gutter={[16, 16]} ref={cardsContainerRef}> <Row gutter={[16, 16]} key={cardColSpan}>
{tableData.map((record) => { {tableData.map((record) => {
if (record?._id == undefined) { if (record?._id == undefined) {
return null return null
} }
return ( return (
<Col <Col span={cardColSpan} key={record._id}>
xs={24}
sm={24}
md={24}
lg={12}
xl={12}
xxl={8}
xxxl={6}
key={record._id}
>
<div style={{ width: '100%', overflow: 'hidden' }}> <div style={{ width: '100%', overflow: 'hidden' }}>
<RowForm <RowForm
record={record} record={record}
@ -1141,11 +1195,22 @@ const ObjectTable = forwardRef(
) )
const tableContent = ( const tableContent = (
<Flex gap={'middle'} vertical style={{ flex: 1, minWidth: 0 }}> <Flex
<Splitter className={'farmcontrol-splitter'}> gap={'middle'}
vertical
style={{ flex: 1, minWidth: 0, minHeight: 0 }}
>
<Splitter
className={'farmcontrol-splitter'}
onResize={measureCardColSpan}
onResizeEnd={measureCardColSpan}
>
<Splitter.Panel> <Splitter.Panel>
{cards ? ( {cards ? (
<div className='objectTableCardsContainer'> <div
className='objectTableCardsContainer'
ref={objectTableCardsContainerRef}
>
<Spin <Spin
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
spinning={loading} spinning={loading}

View File

@ -68,6 +68,13 @@ export const User = {
readOnly: true, readOnly: true,
columnWidth: 180 columnWidth: 180
}, },
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{ {
name: 'name', name: 'name',
label: 'Name', label: 'Name',
@ -76,12 +83,13 @@ export const User = {
type: 'text', type: 'text',
columnWidth: 200 columnWidth: 200
}, },
{ {
name: 'updatedAt', name: 'username',
label: 'Updated At', label: 'Username',
type: 'dateTime', required: true,
readOnly: true, type: 'text',
columnWidth: 175 columnWidth: 150
}, },
{ {
@ -90,13 +98,7 @@ export const User = {
type: 'text', type: 'text',
columnWidth: 130 columnWidth: 130
}, },
{
name: 'username',
label: 'Username',
required: true,
type: 'text',
columnWidth: 150
},
{ {
name: 'lastName', name: 'lastName',
label: 'Last Name', label: 'Last Name',