Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5bf3ba0
feat(forms): add resizable columns to subviews
grantfitzsimmons Oct 3, 2026
62f393c
fix(forms): simplify subview logic
grantfitzsimmons Oct 3, 2026
5f26509
fix(forms): stop losing subview table header
grantfitzsimmons Oct 3, 2026
b612355
fix(forms): stabilize subview width measurement
grantfitzsimmons Oct 3, 2026
ae69023
fix(forms): preserve subview column spans
grantfitzsimmons Oct 3, 2026
f16394f
feat(forms): support accessible subview column resizing
grantfitzsimmons Oct 3, 2026
e5b99de
fix(forms): fit subview action column on load
grantfitzsimmons Oct 3, 2026
a543759
fix(forms): size subview headers across two lines
grantfitzsimmons Oct 3, 2026
893d6aa
fix(forms): center wrapped subview headings
grantfitzsimmons Oct 3, 2026
bb60b40
fix(forms): ignore sort state when sizing headers
grantfitzsimmons Oct 3, 2026
906d88c
fix(forms): sorting measures active headers only
grantfitzsimmons Oct 3, 2026
237b247
refactor(forms): simplify sorted header sizing
grantfitzsimmons Oct 3, 2026
a28e87a
fix(forms): center subview column headings
grantfitzsimmons Oct 3, 2026
b6a424b
fix(forms): restore subview paginator padding
grantfitzsimmons Oct 3, 2026
84084dd
fix(forms): keep subview actions visible when narrow
grantfitzsimmons Oct 3, 2026
f126f46
fix(forms): increase grab area for subview resizing
grantfitzsimmons Oct 3, 2026
f311505
fix(forms): handle custom columnDef=table
grantfitzsimmons Oct 3, 2026
0d62e15
fix(forms): check collapse status
grantfitzsimmons Oct 5, 2026
a175ba1
fix(forms): handle collapsed in measurement
grantfitzsimmons Oct 5, 2026
e72183b
Merge branch 'main' into issue-8604
grantfitzsimmons Oct 5, 2026
5e6eeb9
fix(forms): cleanup
grantfitzsimmons Oct 6, 2026
5128856
Refactor: Add a form table column util file
CarolineDenis Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 109 additions & 34 deletions specifyweb/frontend/js_src/lib/components/FormCells/FormTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { f } from '../../utils/functools';
import type { IR, RA } from '../../utils/types';
import { sortFunction } from '../../utils/utils';
import { Button } from '../Atoms/Button';
import { columnDefinitionsToCss, DataEntry } from '../Atoms/DataEntry';
import { DataEntry } from '../Atoms/DataEntry';
import { icons } from '../Atoms/Icons';
import { Link } from '../Atoms/Link';
import { useAttachment } from '../Attachments/Plugin';
Expand Down Expand Up @@ -41,6 +41,10 @@ import { AttachmentPluginSkeleton } from '../SkeletonLoaders/AttachmentPlugin';
import { relationshipIsToMany } from '../WbPlanView/mappingHelpers';
import { COJODialog } from './COJODialog';
import { FormCell } from './index';
import {
FormTableColumnResizeHandle,
useFormTableColumns,
} from './useFormTableColumns';

const cellToLabel = (
table: SpecifyTable,
Expand All @@ -59,7 +63,7 @@ const cellToLabel = (
});

const cellClassName =
'sticky top-0 bg-[color:var(--form-foreground)] z-10 h-full -mx-1 pl-1 pt-1';
'sticky top-0 bg-[color:var(--form-foreground)] z-10 h-full -mx-1 px-1 py-1 border-b border-gray-500';

// REFACTOR: split this component into smaller
/**
Expand Down Expand Up @@ -111,17 +115,20 @@ export function FormTable<SCHEMA extends AnySchema>({
}
);

const resources =
sortConfig === undefined
? // Note, resources might be sorted by the back-end
unsortedResources
: Array.from(unsortedResources).sort(
sortFunction(
// FEATURE: handle related fields
(resource) => resource.get(sortConfig.sortField),
!sortConfig.ascending
)
);
const resources = React.useMemo(
() =>
sortConfig === undefined
? // Note, resources might be sorted by the back-end
unsortedResources
: Array.from(unsortedResources).sort(
sortFunction(
// FEATURE: handle related fields
(resource) => resource.get(sortConfig.sortField),
!sortConfig.ascending
)
),
[sortConfig, unsortedResources]
);

// When added a new resource, focus that row
const addedResource = React.useRef<SpecifyResource<SCHEMA> | undefined>(
Expand Down Expand Up @@ -243,10 +250,22 @@ export function FormTable<SCHEMA extends AnySchema>({
mode !== 'view' && typeof handleDelete === 'function';
const displayViewButton = !isDependent;

const scrollerRef = React.useRef<HTMLDivElement | null>(null);
const columnSizing = useFormTableColumns({
flexibleSubGridColumnWidth,
hasResources: resources.length > 0,
isCollapsed,
measurementKey: unsortedResources,
sortConfig,
viewDefinition: collapsedViewDefinition,
});
const {
gridRef: scrollerRef,
gridTemplateColumns,
scrollContainerRef,
} = columnSizing;
const { isFetching, handleScroll } = useInfiniteScroll(
handleFetchMore,
scrollerRef
scrollContainerRef
);

const [maxHeight] = userPreferences.use('form', 'formTable', 'maxHeight');
Expand Down Expand Up @@ -290,23 +309,22 @@ export function FormTable<SCHEMA extends AnySchema>({
isCollapsed
? 'hidden'
: showSubviewBorders
? 'overflow-x-auto border border-gray-500 border-t-0 rounded-b pl-1 pr-1 pb-1'
: 'overflow-x-auto pl-1 pr-1 pb-1'
? 'overflow-auto border border-gray-500 border-t-0 rounded-b pl-1 pr-1 pb-1'
: 'overflow-auto pl-1 pr-1 pb-1'
}
ref={scrollContainerRef}
style={{ maxHeight: `${maxHeight}px` }}
onScroll={handleScroll}
>
<DataEntry.Grid
className="sticky w-fit"
className="w-full gap-1 pt-0"
display="inline"
flexibleColumnWidth={flexibleColumnWidth}
forwardRef={scrollerRef}
role="table"
style={{
gridTemplateColumns: `min-content ${columnDefinitionsToCss(
collapsedViewDefinition.columns,
flexibleSubGridColumnWidth
)} min-content`,
maxHeight: `${maxHeight}px`,
gridTemplateColumns,
width: '100%',
}}
viewDefinition={collapsedViewDefinition}
>
Expand All @@ -322,6 +340,7 @@ export function FormTable<SCHEMA extends AnySchema>({
<span className="sr-only">{commonText.expand()}</span>
</div>
{collapsedViewDefinition.rows[0].map((cell, index) => {
const columnIndex = index;
const { text, title } = cellToLabel(
relationship.relatedTable,
cell
Expand All @@ -331,38 +350,66 @@ export function FormTable<SCHEMA extends AnySchema>({
const fieldName = isSortable
? cell.fieldNames?.join(backboneFieldSeparator)
: undefined;
const label = (
<span
className="block min-w-0 overflow-hidden whitespace-normal break-normal text-center"
style={{
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
}}
data-subview-header-label
title={text}
>
{text}
</span>
);
return (
<DataEntry.Cell
align="center"
className={cellClassName}
className={`${cellClassName} relative min-w-0 justify-center`}
colSpan={cell.colSpan}
key={index}
data-subview-header-col={columnIndex}
role="columnheader"
title={title}
verticalAlign={cell.verticalAlign}
visible
>
{isSortable && typeof fieldName === 'string' ? (
<Button.LikeLink
className="flex w-full min-w-0 items-center justify-center gap-1 overflow-hidden"
onClick={(): void =>
setSortConfig({
sortField: fieldName,
ascending: !(sortConfig?.ascending ?? false),
})
}
>
{text}
<SortIndicator
fieldName={fieldName}
sortConfig={sortConfig}
/>
{label}
<span className="shrink-0" data-subview-sort-indicator>
<SortIndicator
fieldName={fieldName}
sortConfig={sortConfig}
/>
</span>
</Button.LikeLink>
) : (
text
label
)}
<FormTableColumnResizeHandle
columnIndex={columnIndex}
columnSizing={columnSizing}
/>
</DataEntry.Cell>
);
})}
<div
aria-hidden="true"
className={cellClassName}
role="columnheader"
style={{ gridColumn: '-3 / -2' }}
/>
<div className={cellClassName} role="columnheader">
<span className="sr-only">{commonText.actions()}</span>
</div>
Expand All @@ -373,7 +420,11 @@ export function FormTable<SCHEMA extends AnySchema>({
<div className="contents" role="row">
{isExpanded[resource.cid] === true ? (
<>
<div className="h-full" role="cell">
<div
className="h-full"
role="cell"
style={{ gridColumn: '1 / 2' }}
>
<Button.Small
aria-label={commonText.collapse()}
className="h-full"
Expand All @@ -390,8 +441,12 @@ export function FormTable<SCHEMA extends AnySchema>({
</div>
<DataEntry.Cell
align="left"
colSpan={collapsedViewDefinition.columns.length}
className="border-y border-gray-400 py-1"
colSpan={collapsedViewDefinition.columns.length + 1}
role="cell"
style={{
gridColumn: `2 / span ${collapsedViewDefinition.columns.length + 1}`,
}}
tabIndex={-1}
verticalAlign="stretch"
visible
Expand Down Expand Up @@ -443,7 +498,13 @@ export function FormTable<SCHEMA extends AnySchema>({
}
>
{collapsedViewDefinition.isAttachmentPlugin ? (
<div className="flex gap-8" role="cell">
<div
className="flex gap-8"
role="cell"
style={{
gridColumn: `span ${collapsedViewDefinition.columns.length} / span ${collapsedViewDefinition.columns.length}`,
}}
>
<Attachment resource={resource} />
</div>
) : (
Expand All @@ -461,8 +522,10 @@ export function FormTable<SCHEMA extends AnySchema>({
) => (
<DataEntry.Cell
align={align}
className="min-w-0 [&_input]:min-w-0 [&_input]:max-w-full [&_input]:text-ellipsis [&_select]:min-w-0 [&_select]:max-w-full [&_textarea]:min-w-0 [&_textarea]:max-w-full"
colSpan={colSpan}
key={index}
data-subview-cell-col={index}
role="cell"
verticalAlign={verticalAlign}
visible={visible}
Expand All @@ -486,7 +549,19 @@ export function FormTable<SCHEMA extends AnySchema>({
</ReadOnlyContext.Provider>
</>
)}
<div className="flex h-full flex-col gap-2" role="cell">
{isExpanded[resource.cid] !== true && (
<div
aria-hidden="true"
className="border-b border-gray-200"
role="cell"
style={{ gridColumn: '-3 / -2' }}
/>
)}
<div
className="flex h-full flex-col gap-2"
role="cell"
style={{ gridColumn: '-2 / -1' }}
>
{displayViewButton &&
isExpanded[resource.cid] === true &&
!resource.isNew() ? (
Expand Down
Loading
Loading