Add no value column on Kanban (#6252)
<img width="1512" alt="image" src="https://github.com/user-attachments/assets/9fcdd5ca-4329-467c-ada8-4dd5d45be259"> Open questions: - the Tag component does not match Figma in term of style and API for "transparent" | "outline". We need to discuss with @Bonapara what is the desired behavior here - right now opportunity.stage is not nullable. We need to discuss with @FelixMalfait and @Bonapara what we want here. I would advocate to make a it nullable for now until we introduce settings on select fields. custom select are nullable and it could be confusing for the user Follow up: - enhance tests on Tags - add story to cover the No Value column on record board
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import { useContext, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
||||
import { useContext, useRef } from 'react';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext, useState } from 'react';
|
||||
import { IconDotsVertical, Tag } from 'twenty-ui';
|
||||
|
||||
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
|
||||
import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
|
||||
@ -79,14 +80,23 @@ export const RecordBoardColumnHeader = () => {
|
||||
>
|
||||
<Tag
|
||||
onClick={handleBoardColumnMenuOpen}
|
||||
color={columnDefinition.color}
|
||||
variant={
|
||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
||||
? 'solid'
|
||||
: 'outline'
|
||||
}
|
||||
color={
|
||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
||||
? columnDefinition.color
|
||||
: 'transparent'
|
||||
}
|
||||
text={columnDefinition.title}
|
||||
/>
|
||||
{!!boardColumnTotal && <StyledAmount>${boardColumnTotal}</StyledAmount>}
|
||||
{!isHeaderHovered && (
|
||||
<StyledNumChildren>{recordCount}</StyledNumChildren>
|
||||
)}
|
||||
{isHeaderHovered && (
|
||||
{isHeaderHovered && columnDefinition.actions.length > 0 && (
|
||||
<StyledHeaderActions>
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
@ -96,7 +106,7 @@ export const RecordBoardColumnHeader = () => {
|
||||
</StyledHeaderActions>
|
||||
)}
|
||||
</StyledHeader>
|
||||
{isBoardColumnMenuOpen && (
|
||||
{isBoardColumnMenuOpen && columnDefinition.actions.length > 0 && (
|
||||
<RecordBoardColumnDropdownMenu
|
||||
onClose={handleBoardColumnMenuClose}
|
||||
stageId={columnDefinition.id}
|
||||
|
||||
@ -2,11 +2,30 @@ import { ThemeColor } from 'twenty-ui';
|
||||
|
||||
import { RecordBoardColumnAction } from '@/object-record/record-board/types/RecordBoardColumnAction';
|
||||
|
||||
export type RecordBoardColumnDefinition = {
|
||||
id: string;
|
||||
title: string;
|
||||
value: string;
|
||||
export const enum RecordBoardColumnDefinitionType {
|
||||
Value = 'value',
|
||||
NoValue = 'no-value',
|
||||
}
|
||||
|
||||
export type RecordBoardColumnDefinitionNoValue = {
|
||||
id: 'no-value';
|
||||
type: RecordBoardColumnDefinitionType.NoValue;
|
||||
title: 'No Value';
|
||||
position: number;
|
||||
color: ThemeColor;
|
||||
value: null;
|
||||
actions: RecordBoardColumnAction[];
|
||||
};
|
||||
|
||||
export type RecordBoardColumnDefinitionValue = {
|
||||
id: string;
|
||||
type: RecordBoardColumnDefinitionType.Value;
|
||||
title: string;
|
||||
value: string;
|
||||
color: ThemeColor;
|
||||
position: number;
|
||||
actions: RecordBoardColumnAction[];
|
||||
};
|
||||
|
||||
export type RecordBoardColumnDefinition =
|
||||
| RecordBoardColumnDefinitionValue
|
||||
| RecordBoardColumnDefinitionNoValue;
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
FieldSelectMetadata,
|
||||
FieldTextMetadata,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { type } from 'os';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
mockedCompanyObjectMetadataItem,
|
||||
@ -44,6 +43,7 @@ export const selectFieldDefinition: FieldDefinition<FieldSelectMetadata> = {
|
||||
metadata: {
|
||||
fieldName: 'accountOwner',
|
||||
options: [{ label: 'Elon Musk', color: 'blue', value: 'userId' }],
|
||||
isNullable: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useClearField } from '@/object-record/record-field/hooks/useClearField';
|
||||
@ -98,14 +98,16 @@ export const SelectFieldInput = ({
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<MenuItemSelectTag
|
||||
key={`No ${fieldDefinition.label}`}
|
||||
selected={false}
|
||||
text={`No ${fieldDefinition.label}`}
|
||||
color="transparent"
|
||||
variant="outline"
|
||||
onClick={handleClearField}
|
||||
/>
|
||||
{fieldDefinition.metadata.isNullable && (
|
||||
<MenuItemSelectTag
|
||||
key={`No ${fieldDefinition.label}`}
|
||||
selected={false}
|
||||
text={`No ${fieldDefinition.label}`}
|
||||
color="transparent"
|
||||
variant="outline"
|
||||
onClick={handleClearField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{optionsInDropDown.map((option) => {
|
||||
return (
|
||||
|
||||
@ -128,6 +128,7 @@ export type FieldSelectMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
options: { label: string; color: ThemeColor; value: string }[];
|
||||
isNullable: boolean;
|
||||
};
|
||||
|
||||
export type FieldMultiSelectMetadata = {
|
||||
|
||||
@ -16,7 +16,7 @@ export const RecordIndexBoardColumnLoaderEffect = ({
|
||||
}: {
|
||||
recordBoardId: string;
|
||||
objectNameSingular: string;
|
||||
boardFieldSelectValue: string;
|
||||
boardFieldSelectValue: string | null;
|
||||
boardFieldMetadataId: string | null;
|
||||
columnId: string;
|
||||
}) => {
|
||||
|
||||
@ -45,6 +45,15 @@ export const RecordIndexBoardDataLoader = ({
|
||||
columnId={columnIds[index]}
|
||||
/>
|
||||
))}
|
||||
{recordIndexKanbanFieldMetadataItem?.isNullable && (
|
||||
<RecordIndexBoardColumnLoaderEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
boardFieldMetadataId={recordIndexKanbanFieldMetadataId}
|
||||
boardFieldSelectValue={null}
|
||||
recordBoardId={recordBoardId}
|
||||
columnId={'no-value'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,12 +10,13 @@ import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hook
|
||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type UseLoadRecordIndexBoardProps = {
|
||||
objectNameSingular: string;
|
||||
boardFieldMetadataId: string | null;
|
||||
recordBoardId: string;
|
||||
columnFieldSelectValue: string;
|
||||
columnFieldSelectValue: string | null;
|
||||
columnId: string;
|
||||
};
|
||||
|
||||
@ -51,9 +52,11 @@ export const useLoadRecordIndexBoardColumn = ({
|
||||
|
||||
const filter = {
|
||||
...requestFilters,
|
||||
[recordIndexKanbanFieldMetadataItem?.name ?? '']: {
|
||||
in: [columnFieldSelectValue],
|
||||
},
|
||||
[recordIndexKanbanFieldMetadataItem?.name ?? '']: isDefined(
|
||||
columnFieldSelectValue,
|
||||
)
|
||||
? { in: [columnFieldSelectValue] }
|
||||
: { is: 'NULL' },
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { IconPencil } from 'twenty-ui';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import {
|
||||
RecordBoardColumnDefinition,
|
||||
RecordBoardColumnDefinitionNoValue,
|
||||
RecordBoardColumnDefinitionType,
|
||||
RecordBoardColumnDefinitionValue,
|
||||
} from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const computeRecordBoardColumnDefinitionsFromObjectMetadata = (
|
||||
@ -25,20 +30,42 @@ export const computeRecordBoardColumnDefinitionsFromObjectMetadata = (
|
||||
);
|
||||
}
|
||||
|
||||
return selectFieldMetadataItem.options.map((selectOption) => ({
|
||||
id: selectOption.id,
|
||||
title: selectOption.label,
|
||||
value: selectOption.value,
|
||||
color: selectOption.color,
|
||||
position: selectOption.position,
|
||||
actions: [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit from settings',
|
||||
icon: IconPencil,
|
||||
position: 0,
|
||||
callback: navigateToSelectSettings,
|
||||
},
|
||||
],
|
||||
}));
|
||||
const valueColumns = selectFieldMetadataItem.options.map(
|
||||
(selectOption) =>
|
||||
({
|
||||
id: selectOption.id,
|
||||
type: RecordBoardColumnDefinitionType.Value,
|
||||
title: selectOption.label,
|
||||
value: selectOption.value,
|
||||
color: selectOption.color,
|
||||
position: selectOption.position,
|
||||
actions: [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit from settings',
|
||||
icon: IconPencil,
|
||||
position: 0,
|
||||
callback: navigateToSelectSettings,
|
||||
},
|
||||
],
|
||||
}) satisfies RecordBoardColumnDefinitionValue,
|
||||
);
|
||||
|
||||
const noValueColumn = {
|
||||
id: 'no-value',
|
||||
title: 'No Value',
|
||||
type: RecordBoardColumnDefinitionType.NoValue,
|
||||
value: null,
|
||||
actions: [],
|
||||
position:
|
||||
selectFieldMetadataItem.options
|
||||
.map((option) => option.position)
|
||||
.reduce((a, b) => Math.max(a, b), 0) + 1,
|
||||
} satisfies RecordBoardColumnDefinitionNoValue;
|
||||
|
||||
if (selectFieldMetadataItem.isNullable === true) {
|
||||
return [...valueColumns, noValueColumn];
|
||||
}
|
||||
|
||||
return valueColumns;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user