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:
Charles Bochet
2024-07-15 17:48:17 +02:00
committed by GitHub
parent aed0bf41ce
commit 2cd624a5ab
18 changed files with 272 additions and 51 deletions

View File

@ -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';

View File

@ -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}

View File

@ -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;

View File

@ -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,
},
};

View File

@ -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 (

View File

@ -128,6 +128,7 @@ export type FieldSelectMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
options: { label: string; color: ThemeColor; value: string }[];
isNullable: boolean;
};
export type FieldMultiSelectMetadata = {

View File

@ -16,7 +16,7 @@ export const RecordIndexBoardColumnLoaderEffect = ({
}: {
recordBoardId: string;
objectNameSingular: string;
boardFieldSelectValue: string;
boardFieldSelectValue: string | null;
boardFieldMetadataId: string | null;
columnId: string;
}) => {

View File

@ -45,6 +45,15 @@ export const RecordIndexBoardDataLoader = ({
columnId={columnIds[index]}
/>
))}
{recordIndexKanbanFieldMetadataItem?.isNullable && (
<RecordIndexBoardColumnLoaderEffect
objectNameSingular={objectNameSingular}
boardFieldMetadataId={recordIndexKanbanFieldMetadataId}
boardFieldSelectValue={null}
recordBoardId={recordBoardId}
columnId={'no-value'}
/>
)}
</>
);
};

View File

@ -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 {

View File

@ -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;
};