mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-25 13:02:15 +03:00
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:
parent
9299ad1432
commit
201a2c8acc
@ -9,7 +9,6 @@ import { availableRecordBoardDeprecatedCardFieldsScopedState } from '@/object-re
|
||||
import { recordBoardCardFieldsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedCardFieldsScopedState';
|
||||
import { recordBoardFiltersScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedFiltersScopedState';
|
||||
import { recordBoardSortsScopedState } from '@/object-record/record-board-deprecated/states/recordBoardDeprecatedSortsScopedState';
|
||||
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
|
||||
import { useSetRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useSetRecoilScopedStateV2';
|
||||
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
@ -61,11 +60,7 @@ export const HooksCompanyBoardEffect = ({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const availableTableColumns = columnDefinitions.filter(
|
||||
filterAvailableTableColumns,
|
||||
);
|
||||
|
||||
setAvailableBoardCardFields(availableTableColumns);
|
||||
setAvailableBoardCardFields(columnDefinitions);
|
||||
}, [columnDefinitions, setAvailableBoardCardFields]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { BoardFieldDefinition } from '@/object-record/record-board-deprecated/types/BoardFieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
@ -7,7 +9,7 @@ export const mapBoardFieldDefinitionsToViewFields = (
|
||||
): ViewField[] => {
|
||||
return fieldsDefinitions.map(
|
||||
(fieldDefinition): ViewField => ({
|
||||
id: fieldDefinition.viewFieldId || '',
|
||||
id: fieldDefinition.viewFieldId || v4(),
|
||||
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
||||
size: 0,
|
||||
position: fieldDefinition.position,
|
||||
|
@ -3,6 +3,7 @@ import { useMemo } from 'react';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
|
||||
import { Nullable } from '~/types/Nullable';
|
||||
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
@ -25,13 +26,15 @@ export const useColumnDefinitionsFromFieldMetadata = (
|
||||
const columnDefinitions: ColumnDefinition<FieldMetadata>[] = useMemo(
|
||||
() =>
|
||||
objectMetadataItem
|
||||
? activeFieldMetadataItems.map((field, index) =>
|
||||
? activeFieldMetadataItems
|
||||
.map((field, index) =>
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
position: index,
|
||||
field,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
)
|
||||
.filter(filterAvailableTableColumns)
|
||||
: [],
|
||||
[activeFieldMetadataItems, objectMetadataItem],
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
FieldMetadataItemAsFieldDefinitionProps,
|
||||
formatFieldMetadataItemAsFieldDefinition,
|
||||
} from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
|
||||
@ -15,14 +16,22 @@ export const formatFieldMetadataItemAsColumnDefinition = ({
|
||||
objectMetadataItem,
|
||||
showLabel,
|
||||
labelWidth,
|
||||
}: FieldMetadataItemAsColumnDefinitionProps): ColumnDefinition<FieldMetadata> => ({
|
||||
}: FieldMetadataItemAsColumnDefinitionProps): ColumnDefinition<FieldMetadata> => {
|
||||
const isLabelIdentifier = isLabelIdentifierField({
|
||||
fieldMetadataItem: field,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
...formatFieldMetadataItemAsFieldDefinition({
|
||||
field,
|
||||
objectMetadataItem,
|
||||
showLabel,
|
||||
labelWidth,
|
||||
}),
|
||||
position,
|
||||
position: isLabelIdentifier ? 0 : position,
|
||||
size: 100,
|
||||
isLabelIdentifier,
|
||||
isVisible: true,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,10 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
|
||||
export const getLabelIdentifierFieldMetadataItem = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
'fields' | 'labelIdentifierFieldMetadataId'
|
||||
>,
|
||||
): FieldMetadataItem | undefined =>
|
||||
objectMetadataItem.fields.find((fieldMetadataItem) =>
|
||||
isLabelIdentifierField({
|
||||
|
@ -26,6 +26,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
||||
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
|
||||
import { useRecordBoardDeprecatedCardFieldsInternal } from '../../hooks/internal/useRecordBoardDeprecatedCardFieldsInternal';
|
||||
import { BoardColumnDefinition } from '../../types/BoardColumnDefinition';
|
||||
@ -113,11 +114,12 @@ export const RecordBoardDeprecatedOptionsDropdownContent = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderFields = [...visibleBoardCardFields];
|
||||
const [removed] = reorderFields.splice(result.source.index - 1, 1);
|
||||
reorderFields.splice(result.destination.index - 1, 0, removed);
|
||||
const reorderedFields = moveArrayItem(visibleBoardCardFields, {
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
handleFieldsReorder(reorderFields);
|
||||
handleFieldsReorder(reorderedFields);
|
||||
},
|
||||
[handleFieldsReorder, visibleBoardCardFields],
|
||||
);
|
||||
@ -217,10 +219,9 @@ export const RecordBoardDeprecatedOptionsDropdownContent = ({
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Visible"
|
||||
fields={visibleBoardCardFields}
|
||||
isVisible={true}
|
||||
onVisibilityChange={handleFieldVisibilityChange}
|
||||
isDraggable={true}
|
||||
isDraggable
|
||||
onDragEnd={handleReorderField}
|
||||
onVisibilityChange={handleFieldVisibilityChange}
|
||||
/>
|
||||
)}
|
||||
{hasVisibleFields && hasHiddenFields && <DropdownMenuSeparator />}
|
||||
@ -228,9 +229,8 @@ export const RecordBoardDeprecatedOptionsDropdownContent = ({
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Hidden"
|
||||
fields={hiddenBoardCardFields}
|
||||
isVisible={false}
|
||||
onVisibilityChange={handleFieldVisibilityChange}
|
||||
isDraggable={false}
|
||||
onVisibilityChange={handleFieldVisibilityChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -74,25 +74,29 @@ export const RecordIndexContainer = ({
|
||||
const onViewFieldsChange = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(viewFields: ViewField[]) => {
|
||||
setTableColumns(
|
||||
mapViewFieldsToColumnDefinitions(viewFields, columnDefinitions),
|
||||
const newFieldDefinitions = mapViewFieldsToColumnDefinitions({
|
||||
viewFields,
|
||||
columnDefinitions,
|
||||
});
|
||||
|
||||
setTableColumns(newFieldDefinitions);
|
||||
|
||||
const newRecordIndexFieldDefinitions = newFieldDefinitions.filter(
|
||||
(boardField) => !boardField.isLabelIdentifier,
|
||||
);
|
||||
|
||||
const existingRecordIndexFieldDefinitions = snapshot
|
||||
.getLoadable(recordIndexFieldDefinitionsState)
|
||||
.getValue();
|
||||
|
||||
const newFieldDefinitions = mapViewFieldsToColumnDefinitions(
|
||||
viewFields,
|
||||
columnDefinitions,
|
||||
);
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
existingRecordIndexFieldDefinitions,
|
||||
newFieldDefinitions,
|
||||
newRecordIndexFieldDefinitions,
|
||||
)
|
||||
)
|
||||
set(recordIndexFieldDefinitionsState, newFieldDefinitions);
|
||||
) {
|
||||
set(recordIndexFieldDefinitionsState, newRecordIndexFieldDefinitions);
|
||||
}
|
||||
},
|
||||
[columnDefinitions, setTableColumns],
|
||||
);
|
||||
|
@ -5,7 +5,6 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
|
||||
type RecordIndexTableContainerEffectProps = {
|
||||
@ -32,7 +31,7 @@ export const RecordIndexTableContainerEffect = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { columnDefinitions, filterDefinitions, sortDefinitions } =
|
||||
const { columnDefinitions } =
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
const { setEntityCountInCurrentView } = useViewBar({
|
||||
@ -40,18 +39,8 @@ export const RecordIndexTableContainerEffect = ({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const availableTableColumns = columnDefinitions.filter(
|
||||
filterAvailableTableColumns,
|
||||
);
|
||||
|
||||
setAvailableTableColumns(availableTableColumns);
|
||||
}, [
|
||||
columnDefinitions,
|
||||
objectMetadataItem,
|
||||
sortDefinitions,
|
||||
filterDefinitions,
|
||||
setAvailableTableColumns,
|
||||
]);
|
||||
setAvailableTableColumns(columnDefinitions);
|
||||
}, [columnDefinitions, setAvailableTableColumns]);
|
||||
|
||||
const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector());
|
||||
|
||||
|
@ -159,10 +159,9 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Visible"
|
||||
fields={visibleRecordFields}
|
||||
isVisible={true}
|
||||
onVisibilityChange={handleChangeFieldVisibility}
|
||||
isDraggable={true}
|
||||
isDraggable
|
||||
onDragEnd={handleReorderFields}
|
||||
onVisibilityChange={handleChangeFieldVisibility}
|
||||
/>
|
||||
{hiddenRecordFields.length > 0 && (
|
||||
<>
|
||||
@ -170,9 +169,8 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Hidden"
|
||||
fields={hiddenRecordFields}
|
||||
isVisible={false}
|
||||
onVisibilityChange={handleChangeFieldVisibility}
|
||||
isDraggable={false}
|
||||
onVisibilityChange={handleChangeFieldVisibility}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -9,10 +9,12 @@ import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoar
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
|
||||
import { useViewFields } from '@/views/hooks/internal/useViewFields';
|
||||
import { useViews } from '@/views/hooks/internal/useViews';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type useRecordIndexOptionsForBoardParams = {
|
||||
objectNameSingular: string;
|
||||
@ -40,74 +42,82 @@ export const useRecordIndexOptionsForBoard = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { columnDefinitions: availableColumnDefinitions } =
|
||||
const { columnDefinitions } =
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
// Todo replace this with label identifier logic
|
||||
const columnDefinitions = availableColumnDefinitions
|
||||
.filter(
|
||||
(columnDefinition) => columnDefinition.metadata.fieldName !== 'name',
|
||||
)
|
||||
.filter(filterAvailableTableColumns);
|
||||
const availableColumnDefinitions = useMemo(
|
||||
() =>
|
||||
columnDefinitions.filter(({ isLabelIdentifier }) => !isLabelIdentifier),
|
||||
[columnDefinitions],
|
||||
);
|
||||
|
||||
const recordIndexFieldDefinitionsByKey = useMemo(
|
||||
() =>
|
||||
mapArrayToObject(
|
||||
recordIndexFieldDefinitions,
|
||||
({ fieldMetadataId }) => fieldMetadataId,
|
||||
),
|
||||
[recordIndexFieldDefinitions],
|
||||
);
|
||||
|
||||
const visibleBoardFields = useMemo(
|
||||
() =>
|
||||
columnDefinitions.filter((columnDefinition) => {
|
||||
return recordIndexFieldDefinitions.some(
|
||||
(existingRecordFieldDefinition) => {
|
||||
return (
|
||||
columnDefinition.fieldMetadataId ===
|
||||
existingRecordFieldDefinition.fieldMetadataId &&
|
||||
existingRecordFieldDefinition.isVisible
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
[columnDefinitions, recordIndexFieldDefinitions],
|
||||
recordIndexFieldDefinitions
|
||||
.filter((boardField) => boardField.isVisible)
|
||||
.sort(
|
||||
(boardFieldA, boardFieldB) =>
|
||||
boardFieldA.position - boardFieldB.position,
|
||||
),
|
||||
[recordIndexFieldDefinitions],
|
||||
);
|
||||
|
||||
const hiddenBoardFields = useMemo(
|
||||
() =>
|
||||
columnDefinitions.filter((columnDefinition) => {
|
||||
return !recordIndexFieldDefinitions.some(
|
||||
(existingRecordFieldDefinition) => {
|
||||
return (
|
||||
columnDefinition.fieldMetadataId ===
|
||||
existingRecordFieldDefinition.fieldMetadataId &&
|
||||
existingRecordFieldDefinition.isVisible
|
||||
);
|
||||
},
|
||||
);
|
||||
availableColumnDefinitions
|
||||
.filter(
|
||||
({ fieldMetadataId }) =>
|
||||
!recordIndexFieldDefinitionsByKey[fieldMetadataId]?.isVisible,
|
||||
)
|
||||
.map((availableColumnDefinition) => {
|
||||
const { fieldMetadataId } = availableColumnDefinition;
|
||||
const existingBoardField =
|
||||
recordIndexFieldDefinitionsByKey[fieldMetadataId];
|
||||
|
||||
return {
|
||||
...(existingBoardField || availableColumnDefinition),
|
||||
isVisible: false,
|
||||
};
|
||||
}),
|
||||
[columnDefinitions, recordIndexFieldDefinitions],
|
||||
[availableColumnDefinitions, recordIndexFieldDefinitionsByKey],
|
||||
);
|
||||
|
||||
const handleReorderBoardFields: OnDragEndResponder = useCallback(
|
||||
(result) => {
|
||||
if (
|
||||
!result.destination ||
|
||||
result.destination.index === 1 ||
|
||||
result.source.index === 1
|
||||
) {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderFields = [...recordIndexFieldDefinitions];
|
||||
const [removed] = reorderFields.splice(result.source.index - 1, 1);
|
||||
reorderFields.splice(result.destination.index - 1, 0, removed);
|
||||
const reorderedVisibleBoardFields = moveArrayItem(visibleBoardFields, {
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
const updatedFields = reorderFields.map((field, index) => ({
|
||||
...field,
|
||||
position: index,
|
||||
}));
|
||||
if (isDeeplyEqual(visibleBoardFields, reorderedVisibleBoardFields))
|
||||
return;
|
||||
|
||||
const updatedFields = [
|
||||
...reorderedVisibleBoardFields,
|
||||
...hiddenBoardFields,
|
||||
].map((field, index) => ({ ...field, position: index }));
|
||||
|
||||
setRecordIndexFieldDefinitions(updatedFields);
|
||||
persistViewFields(mapBoardFieldDefinitionsToViewFields(updatedFields));
|
||||
},
|
||||
[
|
||||
hiddenBoardFields,
|
||||
persistViewFields,
|
||||
recordIndexFieldDefinitions,
|
||||
setRecordIndexFieldDefinitions,
|
||||
visibleBoardFields,
|
||||
],
|
||||
);
|
||||
|
||||
@ -120,36 +130,43 @@ export const useRecordIndexOptionsForBoard = ({
|
||||
'size' | 'position'
|
||||
>,
|
||||
) => {
|
||||
const isNewViewField = !recordIndexFieldDefinitions.some(
|
||||
(fieldDefinition) =>
|
||||
fieldDefinition.fieldMetadataId ===
|
||||
updatedFieldDefinition.fieldMetadataId,
|
||||
const isNewViewField = !(
|
||||
updatedFieldDefinition.fieldMetadataId in
|
||||
recordIndexFieldDefinitionsByKey
|
||||
);
|
||||
|
||||
let updatedFieldsDefinitions: ColumnDefinition<FieldMetadata>[];
|
||||
|
||||
if (isNewViewField) {
|
||||
const correspondingFieldDefinition = columnDefinitions.find(
|
||||
(availableTableColumn) =>
|
||||
availableTableColumn.fieldMetadataId ===
|
||||
const correspondingFieldDefinition = availableColumnDefinitions.find(
|
||||
(availableColumnDefinition) =>
|
||||
availableColumnDefinition.fieldMetadataId ===
|
||||
updatedFieldDefinition.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!correspondingFieldDefinition) return;
|
||||
|
||||
const lastVisibleBoardField =
|
||||
visibleBoardFields[visibleBoardFields.length - 1];
|
||||
|
||||
updatedFieldsDefinitions = [
|
||||
...recordIndexFieldDefinitions,
|
||||
{ ...correspondingFieldDefinition, isVisible: true },
|
||||
{
|
||||
...correspondingFieldDefinition,
|
||||
position: lastVisibleBoardField.position + 1,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
updatedFieldsDefinitions = recordIndexFieldDefinitions.map(
|
||||
(exitingFieldDefinition) =>
|
||||
exitingFieldDefinition.fieldMetadataId ===
|
||||
(existingFieldDefinition) =>
|
||||
existingFieldDefinition.fieldMetadataId ===
|
||||
updatedFieldDefinition.fieldMetadataId
|
||||
? {
|
||||
...exitingFieldDefinition,
|
||||
isVisible: !exitingFieldDefinition.isVisible,
|
||||
...existingFieldDefinition,
|
||||
isVisible: !existingFieldDefinition.isVisible,
|
||||
}
|
||||
: exitingFieldDefinition,
|
||||
: existingFieldDefinition,
|
||||
);
|
||||
}
|
||||
|
||||
@ -160,10 +177,12 @@ export const useRecordIndexOptionsForBoard = ({
|
||||
);
|
||||
},
|
||||
[
|
||||
recordIndexFieldDefinitions,
|
||||
recordIndexFieldDefinitionsByKey,
|
||||
setRecordIndexFieldDefinitions,
|
||||
persistViewFields,
|
||||
columnDefinitions,
|
||||
availableColumnDefinitions,
|
||||
visibleBoardFields,
|
||||
recordIndexFieldDefinitions,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
|
||||
export const useRecordIndexOptionsForTable = (recordTableId: string) => {
|
||||
const { getHiddenTableColumnsSelector, getVisibleTableColumnsSelector } =
|
||||
@ -26,11 +27,12 @@ export const useRecordIndexOptionsForTable = (recordTableId: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderFields = [...visibleTableColumns];
|
||||
const [removed] = reorderFields.splice(result.source.index - 1, 1);
|
||||
reorderFields.splice(result.destination.index - 1, 0, removed);
|
||||
const reorderedFields = moveArrayItem(visibleTableColumns, {
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
handleColumnReorder(reorderFields);
|
||||
handleColumnReorder(reorderedFields);
|
||||
},
|
||||
[visibleTableColumns, handleColumnReorder],
|
||||
);
|
||||
|
@ -9,9 +9,6 @@ import { RecordTableColumnDropdownMenu } from './RecordTableColumnDropdownMenu';
|
||||
|
||||
type ColumnHeadWithDropdownProps = {
|
||||
column: ColumnDefinition<FieldMetadata>;
|
||||
isFirstColumn: boolean;
|
||||
isLastColumn: boolean;
|
||||
primaryColumnKey: string;
|
||||
};
|
||||
|
||||
const StyledDropdown = styled(Dropdown)`
|
||||
@ -21,22 +18,12 @@ const StyledDropdown = styled(Dropdown)`
|
||||
|
||||
export const ColumnHeadWithDropdown = ({
|
||||
column,
|
||||
isFirstColumn,
|
||||
isLastColumn,
|
||||
primaryColumnKey,
|
||||
}: ColumnHeadWithDropdownProps) => {
|
||||
return (
|
||||
<StyledDropdown
|
||||
dropdownId={column.fieldMetadataId + '-header'}
|
||||
clickableComponent={<ColumnHead column={column} />}
|
||||
dropdownComponents={
|
||||
<RecordTableColumnDropdownMenu
|
||||
column={column}
|
||||
isFirstColumn={isFirstColumn}
|
||||
isLastColumn={isLastColumn}
|
||||
primaryColumnKey={primaryColumnKey}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={<RecordTableColumnDropdownMenu column={column} />}
|
||||
dropdownOffset={{ x: -1 }}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownHotkeyScope={{ scope: column.fieldMetadataId + '-header' }}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { IconArrowLeft, IconArrowRight, IconEyeOff } from '@/ui/display/icon';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
@ -9,17 +12,23 @@ import { ColumnDefinition } from '../types/ColumnDefinition';
|
||||
|
||||
export type RecordTableColumnDropdownMenuProps = {
|
||||
column: ColumnDefinition<FieldMetadata>;
|
||||
isFirstColumn: boolean;
|
||||
isLastColumn: boolean;
|
||||
primaryColumnKey: string;
|
||||
};
|
||||
|
||||
export const RecordTableColumnDropdownMenu = ({
|
||||
column,
|
||||
isFirstColumn,
|
||||
isLastColumn,
|
||||
primaryColumnKey,
|
||||
}: RecordTableColumnDropdownMenuProps) => {
|
||||
const { getVisibleTableColumnsSelector } = useRecordTableStates();
|
||||
|
||||
const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector());
|
||||
|
||||
const secondVisibleColumn = visibleTableColumns[1];
|
||||
const canMoveLeft =
|
||||
column.fieldMetadataId !== secondVisibleColumn?.fieldMetadataId;
|
||||
|
||||
const lastVisibleColumn = visibleTableColumns[visibleTableColumns.length - 1];
|
||||
const canMoveRight =
|
||||
column.fieldMetadataId !== lastVisibleColumn?.fieldMetadataId;
|
||||
|
||||
const { handleColumnVisibilityChange, handleMoveTableColumn } =
|
||||
useTableColumns();
|
||||
|
||||
@ -27,17 +36,17 @@ export const RecordTableColumnDropdownMenu = ({
|
||||
|
||||
const handleColumnMoveLeft = () => {
|
||||
closeDropdown();
|
||||
if (isFirstColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canMoveLeft) return;
|
||||
|
||||
handleMoveTableColumn('left', column);
|
||||
};
|
||||
|
||||
const handleColumnMoveRight = () => {
|
||||
closeDropdown();
|
||||
if (isLastColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canMoveRight) return;
|
||||
|
||||
handleMoveTableColumn('right', column);
|
||||
};
|
||||
|
||||
@ -46,18 +55,16 @@ export const RecordTableColumnDropdownMenu = ({
|
||||
handleColumnVisibilityChange(column);
|
||||
};
|
||||
|
||||
return column.fieldMetadataId === primaryColumnKey ? (
|
||||
<></>
|
||||
) : (
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
{!isFirstColumn && (
|
||||
{canMoveLeft && (
|
||||
<MenuItem
|
||||
LeftIcon={IconArrowLeft}
|
||||
onClick={handleColumnMoveLeft}
|
||||
text="Move left"
|
||||
/>
|
||||
)}
|
||||
{!isLastColumn && (
|
||||
{canMoveRight && (
|
||||
<MenuItem
|
||||
LeftIcon={IconArrowRight}
|
||||
onClick={handleColumnMoveRight}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ColumnHead } from '@/object-record/record-table/components/ColumnHead';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
@ -10,6 +11,7 @@ import { IconPlus } from '@/ui/display/icon';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||
|
||||
import { ColumnHeadWithDropdown } from './ColumnHeadWithDropdown';
|
||||
|
||||
@ -80,20 +82,19 @@ export const RecordTableHeaderCell = ({
|
||||
column: ColumnDefinition<FieldMetadata>;
|
||||
createRecord: () => void;
|
||||
}) => {
|
||||
const {
|
||||
getResizeFieldOffsetState,
|
||||
getTableColumnsState,
|
||||
getTableColumnsByKeySelector,
|
||||
getVisibleTableColumnsSelector,
|
||||
} = useRecordTableStates();
|
||||
const { getResizeFieldOffsetState, getTableColumnsState } =
|
||||
useRecordTableStates();
|
||||
|
||||
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
|
||||
getResizeFieldOffsetState(),
|
||||
);
|
||||
|
||||
const tableColumns = useRecoilValue(getTableColumnsState());
|
||||
const tableColumnsByKey = useRecoilValue(getTableColumnsByKeySelector());
|
||||
const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector());
|
||||
const tableColumnsByKey = useMemo(
|
||||
() =>
|
||||
mapArrayToObject(tableColumns, ({ fieldMetadataId }) => fieldMetadataId),
|
||||
[tableColumns],
|
||||
);
|
||||
|
||||
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
||||
number | null
|
||||
@ -108,10 +109,6 @@ export const RecordTableHeaderCell = ({
|
||||
|
||||
const [iconVisibility, setIconVisibility] = useState(false);
|
||||
|
||||
const primaryColumn = visibleTableColumns.find(
|
||||
(column) => column.position === 0,
|
||||
);
|
||||
|
||||
const handleResizeHandlerMove = useCallback(
|
||||
(positionX: number) => {
|
||||
if (!initialPointerPositionX) return;
|
||||
@ -182,13 +179,12 @@ export const RecordTableHeaderCell = ({
|
||||
onMouseEnter={() => setIconVisibility(true)}
|
||||
onMouseLeave={() => setIconVisibility(false)}
|
||||
>
|
||||
<ColumnHeadWithDropdown
|
||||
column={column}
|
||||
isFirstColumn={column.position === 1}
|
||||
isLastColumn={column.position === visibleTableColumns.length - 1}
|
||||
primaryColumnKey={primaryColumn?.fieldMetadataId || ''}
|
||||
/>
|
||||
{iconVisibility && column.position === 0 && (
|
||||
{column.isLabelIdentifier ? (
|
||||
<ColumnHead column={column} />
|
||||
) : (
|
||||
<ColumnHeadWithDropdown column={column} />
|
||||
)}
|
||||
{iconVisibility && !!column.isLabelIdentifier && (
|
||||
<StyledHeaderIcon>
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
|
@ -55,10 +55,8 @@ export const RecordTableRow = ({ recordId, rowIndex }: RecordTableRowProps) => {
|
||||
<StyledTd>
|
||||
<CheckboxCell />
|
||||
</StyledTd>
|
||||
{[...visibleTableColumns]
|
||||
.sort((columnA, columnB) => columnA.position - columnB.position)
|
||||
.map((column, columnIndex) => {
|
||||
return inView ? (
|
||||
{visibleTableColumns.map((column, columnIndex) =>
|
||||
inView ? (
|
||||
<RecordTableCellContext.Provider
|
||||
value={{
|
||||
columnDefinition: column,
|
||||
@ -70,8 +68,8 @@ export const RecordTableRow = ({ recordId, rowIndex }: RecordTableRowProps) => {
|
||||
</RecordTableCellContext.Provider>
|
||||
) : (
|
||||
<td key={column.fieldMetadataId}></td>
|
||||
);
|
||||
})}
|
||||
),
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</RecordTableRowContext.Provider>
|
||||
|
@ -14,7 +14,6 @@ import { allRowsSelectedStatusSelectorScopeMap } from '@/object-record/record-ta
|
||||
import { hiddenTableColumnsSelectorScopeMap } from '@/object-record/record-table/states/selectors/hiddenTableColumnsSelectorScopeMap';
|
||||
import { numberOfTableColumnsSelectorScopeMap } from '@/object-record/record-table/states/selectors/numberOfTableColumnsSelectorScopeMap';
|
||||
import { selectedRowIdsSelectorScopeMap } from '@/object-record/record-table/states/selectors/selectedRowIdsSelectorScopeMap';
|
||||
import { tableColumnsByKeySelectorScopeMap } from '@/object-record/record-table/states/selectors/tableColumnsByKeySelectorScopeMap';
|
||||
import { visibleTableColumnsSelectorScopeMap } from '@/object-record/record-table/states/selectors/visibleTableColumnsSelectorScopeMap';
|
||||
import { softFocusPositionStateScopeMap } from '@/object-record/record-table/states/softFocusPositionStateScopeMap';
|
||||
import { tableColumnsStateScopeMap } from '@/object-record/record-table/states/tableColumnsStateScopeMap';
|
||||
@ -106,10 +105,6 @@ export const useRecordTableStates = (recordTableId?: string) => {
|
||||
selectedRowIdsSelectorScopeMap,
|
||||
scopeId,
|
||||
),
|
||||
getTableColumnsByKeySelector: getSelectorReadOnly(
|
||||
tableColumnsByKeySelectorScopeMap,
|
||||
scopeId,
|
||||
),
|
||||
getVisibleTableColumnsSelector: getSelectorReadOnly(
|
||||
visibleTableColumnsSelectorScopeMap,
|
||||
scopeId,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { availableTableColumnsStateScopeMap } from '@/object-record/record-table/states/availableTableColumnsStateScopeMap';
|
||||
import { tableColumnsStateScopeMap } from '@/object-record/record-table/states/tableColumnsStateScopeMap';
|
||||
import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap';
|
||||
|
||||
import { availableTableColumnsStateScopeMap } from '../availableTableColumnsStateScopeMap';
|
||||
import { tableColumnsStateScopeMap } from '../tableColumnsStateScopeMap';
|
||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||
|
||||
export const hiddenTableColumnsSelectorScopeMap =
|
||||
createSelectorReadOnlyScopeMap({
|
||||
@ -9,19 +9,30 @@ export const hiddenTableColumnsSelectorScopeMap =
|
||||
get:
|
||||
({ scopeId }) =>
|
||||
({ get }) => {
|
||||
const columns = get(tableColumnsStateScopeMap({ scopeId }));
|
||||
const columnKeys = columns.map(
|
||||
const tableColumns = get(tableColumnsStateScopeMap({ scopeId }));
|
||||
const availableColumns = get(
|
||||
availableTableColumnsStateScopeMap({ scopeId }),
|
||||
);
|
||||
const tableColumnsByKey = mapArrayToObject(
|
||||
tableColumns,
|
||||
({ fieldMetadataId }) => fieldMetadataId,
|
||||
);
|
||||
const otherAvailableColumns = get(
|
||||
availableTableColumnsStateScopeMap({ scopeId }),
|
||||
).filter(
|
||||
({ fieldMetadataId }) => !columnKeys.includes(fieldMetadataId),
|
||||
);
|
||||
|
||||
return [
|
||||
...columns.filter((column) => !column.isVisible),
|
||||
...otherAvailableColumns,
|
||||
];
|
||||
const hiddenColumns = availableColumns
|
||||
.filter(
|
||||
({ fieldMetadataId }) =>
|
||||
!tableColumnsByKey[fieldMetadataId]?.isVisible,
|
||||
)
|
||||
.map((availableColumn) => {
|
||||
const { fieldMetadataId } = availableColumn;
|
||||
const existingTableColumn = tableColumnsByKey[fieldMetadataId];
|
||||
|
||||
return {
|
||||
...(existingTableColumn || availableColumn),
|
||||
isVisible: false,
|
||||
};
|
||||
});
|
||||
|
||||
return hiddenColumns;
|
||||
},
|
||||
});
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap';
|
||||
|
||||
import { tableColumnsStateScopeMap } from '../tableColumnsStateScopeMap';
|
||||
|
||||
export const tableColumnsByKeySelectorScopeMap = createSelectorReadOnlyScopeMap(
|
||||
{
|
||||
key: 'tableColumnsByKeySelectorScopeMap',
|
||||
get:
|
||||
({ scopeId }) =>
|
||||
({ get }) =>
|
||||
get(tableColumnsStateScopeMap({ scopeId })).reduce<
|
||||
Record<string, ColumnDefinition<FieldMetadata>>
|
||||
>(
|
||||
(result, column) => ({ ...result, [column.fieldMetadataId]: column }),
|
||||
{},
|
||||
),
|
||||
},
|
||||
);
|
@ -1,8 +1,6 @@
|
||||
import { availableTableColumnsStateScopeMap } from '@/object-record/record-table/states/availableTableColumnsStateScopeMap';
|
||||
import { tableColumnsStateScopeMap } from '@/object-record/record-table/states/tableColumnsStateScopeMap';
|
||||
import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap';
|
||||
|
||||
import { tableColumnsStateScopeMap } from '../tableColumnsStateScopeMap';
|
||||
|
||||
export const visibleTableColumnsSelectorScopeMap =
|
||||
createSelectorReadOnlyScopeMap({
|
||||
key: 'visibleTableColumnsSelectorScopeMap',
|
||||
@ -10,16 +8,8 @@ export const visibleTableColumnsSelectorScopeMap =
|
||||
({ scopeId }) =>
|
||||
({ get }) => {
|
||||
const columns = get(tableColumnsStateScopeMap({ scopeId }));
|
||||
const availableColumnKeys = get(
|
||||
availableTableColumnsStateScopeMap({ scopeId }),
|
||||
).map(({ fieldMetadataId }) => fieldMetadataId);
|
||||
|
||||
return [...columns]
|
||||
.filter(
|
||||
(column) =>
|
||||
column.isVisible &&
|
||||
availableColumnKeys.includes(column.fieldMetadataId),
|
||||
)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
return columns
|
||||
.filter((column) => column.isVisible)
|
||||
.sort((columnA, columnB) => columnA.position - columnB.position);
|
||||
},
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
|
||||
export type ColumnDefinition<T extends FieldMetadata> = FieldDefinition<T> & {
|
||||
size: number;
|
||||
position: number;
|
||||
isLabelIdentifier?: boolean;
|
||||
isVisible?: boolean;
|
||||
viewFieldId?: string;
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import { CardFooter } from '@/ui/layout/card/components/CardFooter';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { mainColorNames, ThemeColor } from '@/ui/theme/constants/colors';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
|
||||
import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption';
|
||||
|
||||
@ -61,10 +62,10 @@ export const SettingsObjectFieldSelectForm = ({
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
const nextOptions = [...values];
|
||||
const [movedOption] = nextOptions.splice(result.source.index, 1);
|
||||
|
||||
nextOptions.splice(result.destination.index, 0, movedOption);
|
||||
const nextOptions = moveArrayItem(values, {
|
||||
fromIndex: result.source.index,
|
||||
toIndex: result.destination.index,
|
||||
});
|
||||
|
||||
onChange(nextOptions);
|
||||
};
|
||||
|
@ -4,7 +4,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
|
||||
import {
|
||||
signInBackgroundMockColumnDefinitions,
|
||||
signInBackgroundMockFilterDefinitions,
|
||||
@ -57,17 +56,13 @@ export const SignInBackgroundMockContainerEffect = ({
|
||||
setAvailableFilterDefinitions?.(signInBackgroundMockFilterDefinitions);
|
||||
setAvailableFieldDefinitions?.(signInBackgroundMockColumnDefinitions);
|
||||
|
||||
const availableTableColumns = signInBackgroundMockColumnDefinitions.filter(
|
||||
filterAvailableTableColumns,
|
||||
);
|
||||
|
||||
setAvailableTableColumns(availableTableColumns);
|
||||
setAvailableTableColumns(signInBackgroundMockColumnDefinitions);
|
||||
|
||||
setTableColumns(
|
||||
mapViewFieldsToColumnDefinitions(
|
||||
signInBackgroundMockViewFields,
|
||||
signInBackgroundMockColumnDefinitions,
|
||||
),
|
||||
mapViewFieldsToColumnDefinitions({
|
||||
viewFields: signInBackgroundMockViewFields,
|
||||
columnDefinitions: signInBackgroundMockColumnDefinitions,
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
setViewObjectMetadataId,
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
|
||||
|
||||
export const signInBackgroundMockColumnDefinitions = [
|
||||
export const signInBackgroundMockColumnDefinitions = (
|
||||
[
|
||||
{
|
||||
position: 0,
|
||||
fieldMetadataId: '20202020-5e4e-4007-a630-8a2617914889',
|
||||
@ -249,7 +252,8 @@ export const signInBackgroundMockColumnDefinitions = [
|
||||
iconName: 'IconMoneybag',
|
||||
isVisible: true,
|
||||
},
|
||||
] as ColumnDefinition<any>[];
|
||||
] satisfies ColumnDefinition<FieldMetadata>[]
|
||||
).filter(filterAvailableTableColumns);
|
||||
|
||||
export const signInBackgroundMockFilterDefinitions = [
|
||||
{
|
||||
|
@ -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 [
|
||||
{
|
||||
const iconButtons = [
|
||||
field.infoTooltipContent
|
||||
? {
|
||||
Icon: IconInfoCircle,
|
||||
onClick: () => handleInfoButtonClick(index),
|
||||
isActive: openToolTipIndex === index,
|
||||
},
|
||||
{
|
||||
}
|
||||
: null,
|
||||
field.isLabelIdentifier
|
||||
? null
|
||||
: {
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!field.infoTooltipContent) {
|
||||
return [
|
||||
{
|
||||
Icon: 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) &&
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 = '';
|
||||
|
||||
const columnDefinitionsByFieldMetadataId = mapArrayToObject(
|
||||
columnDefinitions,
|
||||
({ fieldMetadataId }) => fieldMetadataId,
|
||||
);
|
||||
|
||||
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,
|
||||
const columnDefinitionsFromViewFields = viewFields
|
||||
.map((viewField) => {
|
||||
const correspondingColumnDefinition =
|
||||
columnDefinitionsByFieldMetadataId[viewField.fieldMetadataId];
|
||||
|
||||
if (!correspondingColumnDefinition) return null;
|
||||
|
||||
const { isLabelIdentifier } = correspondingColumnDefinition;
|
||||
|
||||
if (isLabelIdentifier) {
|
||||
labelIdentifierFieldMetadataId =
|
||||
correspondingColumnDefinition.fieldMetadataId;
|
||||
}
|
||||
: null;
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { IconBuildingSkyscraper } from '@/ui/display/icon';
|
||||
@ -14,10 +15,9 @@ import { PageHeader } from '@/ui/layout/page/PageHeader';
|
||||
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
|
||||
import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { useFindOneRecord } from '../../modules/object-record/hooks/useFindOneRecord';
|
||||
|
||||
export const RecordShowPage = () => {
|
||||
const { objectNameSingular, objectRecordId } = useParams<{
|
||||
objectNameSingular: string;
|
||||
@ -32,9 +32,8 @@ export const RecordShowPage = () => {
|
||||
throw new Error(`Record id is not defined`);
|
||||
}
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { labelIdentifierFieldMetadata, objectMetadataItem } =
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const { favorites, createFavorite, deleteFavorite } = useFavorites();
|
||||
|
||||
@ -68,10 +67,15 @@ export const RecordShowPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const labelIdentifierFieldValue =
|
||||
record?.[labelIdentifierFieldMetadata?.name ?? ''];
|
||||
const pageName =
|
||||
objectNameSingular === 'person'
|
||||
? record?.name.firstName + ' ' + record?.name.lastName
|
||||
: record?.name;
|
||||
labelIdentifierFieldMetadata?.type === FieldMetadataType.FullName
|
||||
? [
|
||||
labelIdentifierFieldValue?.firstName,
|
||||
labelIdentifierFieldValue?.lastName,
|
||||
].join(' ')
|
||||
: labelIdentifierFieldValue;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
|
13
packages/twenty-front/src/utils/array/groupArrayItemsBy.ts
Normal file
13
packages/twenty-front/src/utils/array/groupArrayItemsBy.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const groupArrayItemsBy = <Item, Key extends string>(
|
||||
array: Item[],
|
||||
computeGroupKey: (item: Item) => Key,
|
||||
) =>
|
||||
array.reduce<Partial<Record<Key, Item[]>>>((result, item) => {
|
||||
const groupKey = computeGroupKey(item);
|
||||
const previousGroup = result[groupKey] || [];
|
||||
|
||||
return {
|
||||
...result,
|
||||
[groupKey]: [...previousGroup, item],
|
||||
};
|
||||
}, {});
|
@ -0,0 +1,4 @@
|
||||
export const mapArrayToObject = <ArrayItem>(
|
||||
array: ArrayItem[],
|
||||
computeItemKey: (item: ArrayItem) => string,
|
||||
) => Object.fromEntries(array.map((item) => [computeItemKey(item), item]));
|
14
packages/twenty-front/src/utils/array/moveArrayItem.ts
Normal file
14
packages/twenty-front/src/utils/array/moveArrayItem.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const moveArrayItem = <Item>(
|
||||
array: Item[],
|
||||
{ fromIndex, toIndex }: { fromIndex: number; toIndex: number },
|
||||
) => {
|
||||
if (!(fromIndex in array) || !(toIndex in array) || fromIndex === toIndex) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const arrayWithMovedItem = [...array];
|
||||
const [itemToMove] = arrayWithMovedItem.splice(fromIndex, 1);
|
||||
arrayWithMovedItem.splice(toIndex, 0, itemToMove);
|
||||
|
||||
return arrayWithMovedItem;
|
||||
};
|
Loading…
Reference in New Issue
Block a user