feat: display record identifier field as first column in table (#3788)

* feat: display record identifier field as first column in table

& forbid hiding and moving record identifier column

Closes #3303

* refactor: add availableTableColumnKeysSelectorScopeMap

* feat: show plus icon button for label identifier column and dropdown menu for other columns

* fix: use label identifier field value in RecordShowPage title

* refactor: remove availableColumnKeys selector

* refactor: review - compute label identifier logic in mapViewFieldsToColumnDefinitions + remove selectors

* fix: several fixes

* fix: fix board fields isVisible

* fix: fix board fields reordering

* fix: more board fields fixes

* fix: fix hiddenTableColumnsSelectorScopeMap
This commit is contained in:
Thaïs
2024-02-09 08:36:08 -03:00
committed by GitHub
parent 9299ad1432
commit 201a2c8acc
30 changed files with 636 additions and 580 deletions

View File

@ -19,26 +19,25 @@ import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/Sty
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
import { isDefined } from '~/utils/isDefined';
type ViewFieldsVisibilityDropdownSectionProps = {
fields: Omit<ColumnDefinition<FieldMetadata>, 'size'>[];
isDraggable: boolean;
onDragEnd?: OnDragEndResponder;
onVisibilityChange: (
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
) => void;
title: string;
isVisible: boolean;
isDraggable: boolean;
onDragEnd?: OnDragEndResponder;
};
export const ViewFieldsVisibilityDropdownSection = ({
fields,
onVisibilityChange,
title,
isVisible,
isDraggable,
onDragEnd,
onVisibilityChange,
title,
}: ViewFieldsVisibilityDropdownSectionProps) => {
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
onDragEnd?.(result, provided);
@ -47,8 +46,7 @@ export const ViewFieldsVisibilityDropdownSection = ({
const [openToolTipIndex, setOpenToolTipIndex] = useState<number>();
const handleInfoButtonClick = (index: number) => {
if (index === openToolTipIndex) setOpenToolTipIndex(undefined);
else setOpenToolTipIndex(index);
setOpenToolTipIndex(index === openToolTipIndex ? undefined : index);
};
const { getIcon } = useIcons();
@ -57,27 +55,23 @@ export const ViewFieldsVisibilityDropdownSection = ({
index: number,
field: Omit<ColumnDefinition<FieldMetadata>, 'size' | 'position'>,
) => {
if (field.infoTooltipContent) {
return [
{
Icon: IconInfoCircle,
onClick: () => handleInfoButtonClick(index),
isActive: openToolTipIndex === index,
},
{
Icon: field.isVisible ? IconMinus : IconPlus,
onClick: () => onVisibilityChange(field),
},
];
}
if (!field.infoTooltipContent) {
return [
{
Icon: isVisible ? IconMinus : IconPlus,
onClick: () => onVisibilityChange(field),
},
];
}
const iconButtons = [
field.infoTooltipContent
? {
Icon: IconInfoCircle,
onClick: () => handleInfoButtonClick(index),
isActive: openToolTipIndex === index,
}
: null,
field.isLabelIdentifier
? null
: {
Icon: field.isVisible ? IconMinus : IconPlus,
onClick: () => onVisibilityChange(field),
},
].filter(isDefined);
return iconButtons.length ? iconButtons : undefined;
};
const ref = useRef<HTMLDivElement>(null);
@ -89,50 +83,55 @@ export const ViewFieldsVisibilityDropdownSection = ({
},
});
const { nonDraggableItems = [], draggableItems = [] } = isDraggable
? groupArrayItemsBy(fields, ({ isLabelIdentifier }) =>
isLabelIdentifier ? 'nonDraggableItems' : 'draggableItems',
)
: { nonDraggableItems: fields, draggableItems: [] };
return (
<div ref={ref}>
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
<DropdownMenuItemsContainer>
{isDraggable ? (
{nonDraggableItems.map((field, fieldIndex) => (
<MenuItem
key={field.fieldMetadataId}
LeftIcon={getIcon(field.iconName)}
iconButtons={getIconButtons(fieldIndex, field)}
isTooltipOpen={openToolTipIndex === fieldIndex}
text={field.label}
className={`${title}-fixed-item-tooltip-anchor-${fieldIndex}`}
/>
))}
{!!draggableItems.length && (
<DraggableList
onDragEnd={handleOnDrag}
draggableItems={
<>
{[...fields]
.sort((a, b) => a.position - b.position)
.map((field, index) => (
{draggableItems.map((field, index) => {
const fieldIndex = index + nonDraggableItems.length;
return (
<DraggableItem
key={field.fieldMetadataId}
draggableId={field.fieldMetadataId}
index={index + 1}
index={fieldIndex + 1}
itemComponent={
<MenuItemDraggable
key={field.fieldMetadataId}
LeftIcon={getIcon(field.iconName)}
iconButtons={getIconButtons(index + 1, field)}
isTooltipOpen={openToolTipIndex === index + 1}
iconButtons={getIconButtons(fieldIndex, field)}
isTooltipOpen={openToolTipIndex === fieldIndex}
text={field.label}
className={`${title}-draggable-item-tooltip-anchor-${
index + 1
}`}
className={`${title}-draggable-item-tooltip-anchor-${fieldIndex}`}
/>
}
/>
))}
);
})}
</>
}
/>
) : (
fields.map((field, index) => (
<MenuItem
key={field.fieldMetadataId}
LeftIcon={getIcon(field.iconName)}
iconButtons={getIconButtons(index, field)}
isTooltipOpen={openToolTipIndex === index}
text={field.label}
className={`${title}-fixed-item-tooltip-anchor-${index}`}
/>
))
)}
</DropdownMenuItemsContainer>
{isDefined(openToolTipIndex) &&

View File

@ -133,7 +133,7 @@ describe('mapViewFieldsToColumnDefinitions', () => {
},
];
const fieldsMetadata: ColumnDefinition<FieldMetadata>[] = [
const columnDefinitions: ColumnDefinition<FieldMetadata>[] = [
{
fieldMetadataId: '1',
label: 'label 1',
@ -183,10 +183,10 @@ describe('mapViewFieldsToColumnDefinitions', () => {
},
];
const actualColumnDefinitions = mapViewFieldsToColumnDefinitions(
const actualColumnDefinitions = mapViewFieldsToColumnDefinitions({
columnDefinitions,
viewFields,
fieldsMetadata,
);
});
expect(actualColumnDefinitions).toEqual(expectedColumnDefinitions);
});

View File

@ -1,33 +1,66 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { assertNotNull } from '~/utils/assert';
import { ViewField } from '../types/ViewField';
export const mapViewFieldsToColumnDefinitions = (
viewFields: ViewField[],
fieldsMetadata: ColumnDefinition<FieldMetadata>[],
): ColumnDefinition<FieldMetadata>[] => {
return viewFields
.map((viewField) => {
const correspondingFieldMetadata = fieldsMetadata.find(
({ fieldMetadataId }) => viewField.fieldMetadataId === fieldMetadataId,
);
export const mapViewFieldsToColumnDefinitions = ({
columnDefinitions,
viewFields,
}: {
columnDefinitions: ColumnDefinition<FieldMetadata>[];
viewFields: ViewField[];
}): ColumnDefinition<FieldMetadata>[] => {
let labelIdentifierFieldMetadataId = '';
return correspondingFieldMetadata
? {
fieldMetadataId: viewField.fieldMetadataId,
label: correspondingFieldMetadata.label,
metadata: correspondingFieldMetadata.metadata,
infoTooltipContent: correspondingFieldMetadata.infoTooltipContent,
iconName: correspondingFieldMetadata.iconName,
type: correspondingFieldMetadata.type,
position: viewField.position,
size: viewField.size ?? correspondingFieldMetadata.size,
isVisible: viewField.isVisible,
viewFieldId: viewField.id,
}
: null;
const columnDefinitionsByFieldMetadataId = mapArrayToObject(
columnDefinitions,
({ fieldMetadataId }) => fieldMetadataId,
);
const columnDefinitionsFromViewFields = viewFields
.map((viewField) => {
const correspondingColumnDefinition =
columnDefinitionsByFieldMetadataId[viewField.fieldMetadataId];
if (!correspondingColumnDefinition) return null;
const { isLabelIdentifier } = correspondingColumnDefinition;
if (isLabelIdentifier) {
labelIdentifierFieldMetadataId =
correspondingColumnDefinition.fieldMetadataId;
}
return {
fieldMetadataId: viewField.fieldMetadataId,
label: correspondingColumnDefinition.label,
metadata: correspondingColumnDefinition.metadata,
infoTooltipContent: correspondingColumnDefinition.infoTooltipContent,
iconName: correspondingColumnDefinition.iconName,
type: correspondingColumnDefinition.type,
position: isLabelIdentifier ? 0 : viewField.position,
size: viewField.size ?? correspondingColumnDefinition.size,
isLabelIdentifier,
isVisible: isLabelIdentifier || viewField.isVisible,
viewFieldId: viewField.id,
};
})
.filter(assertNotNull);
// No label identifier set for this object
if (!labelIdentifierFieldMetadataId) return columnDefinitionsFromViewFields;
const labelIdentifierIndex = columnDefinitionsFromViewFields.findIndex(
({ fieldMetadataId }) => fieldMetadataId === labelIdentifierFieldMetadataId,
);
// Label identifier field found in view fields
// => move it to the start of the list
return moveArrayItem(columnDefinitionsFromViewFields, {
fromIndex: labelIdentifierIndex,
toIndex: 0,
});
};