Display columns on Record Board (#3626)

* Display columns on Record board

* Fix

* Fix according to review

* Fix
This commit is contained in:
Charles Bochet 2024-01-25 18:21:15 +01:00 committed by GitHub
parent ca6250286a
commit 377fd23c90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 591 additions and 29 deletions

View File

@ -22,7 +22,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => {
<RecoilRoot>
<ObjectNamePluralSetter>
<RecordTableScope
recordTableScopeId={getScopeIdFromComponentId(recordTableId) ?? ''}
recordTableScopeId={getScopeIdFromComponentId(recordTableId)}
onColumnsChange={onColumnsChange}
>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">

View File

@ -1,10 +1,13 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { DragDropContext } 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 { useRecoilValue } from 'recoil';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn';
import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
export type RecordBoardProps = {
@ -37,9 +40,13 @@ const StyledBoardHeader = styled.div`
export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
const boardRef = useRef<HTMLDivElement>(null);
const { getColumnIdsState } = useRecordBoard(recordBoardId);
const columnIds = useRecoilValue(getColumnIdsState());
return (
<RecordBoardScope
recordBoardScopeId={recordBoardId}
recordBoardScopeId={getScopeIdFromComponentId(recordBoardId)}
onColumnsChange={() => {}}
onFieldsChange={() => {}}
>
@ -48,11 +55,10 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
<ScrollWrapper>
<StyledContainer ref={boardRef}>
<DragDropContext onDragEnd={() => {}}>
{[].map((column) => (
{columnIds.map((columnId) => (
<RecordBoardColumn
key={'a'}
recordBoardColumnId={'a'}
columnDefinition={column}
key={columnId}
recordBoardColumnId={columnId}
/>
))}
</DragDropContext>

View File

@ -1,11 +1,14 @@
import { createContext } from 'react';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
type RecordBoardColumnContextProps = {
id: string;
columnDefinition: BoardColumnDefinition;
columnDefinition: RecordBoardColumnDefinition;
isFirstColumn: boolean;
isLastColumn: boolean;
};
export const RecordBoardColumnContext =
createContext<RecordBoardColumnContextProps | null>(null);
createContext<RecordBoardColumnContextProps>(
{} as RecordBoardColumnContextProps,
);

View File

@ -0,0 +1,33 @@
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
import { isFirstRecordBoardColumnFamilyStateScopeMap } from '@/object-record/record-board/states/isFirstRecordBoardColumnFamilyStateScopeMap';
import { isLastRecordBoardColumnFamilyStateScopeMap } from '@/object-record/record-board/states/isLastRecordBoardColumnFamilyStateScopeMap';
import { recordBoardColumnIdsStateScopeMap } from '@/object-record/record-board/states/recordBoardColumnIdsStateScopeMap';
import { recordBoardColumnsFamilySelectorScopeMap } from '@/object-record/record-board/states/selectors/recordBoardColumnsFamilySelectorScopeMap';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getFamilyState } from '@/ui/utilities/recoil-scope/utils/getFamilyState';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { getState } from '@/ui/utilities/recoil-scope/utils/getState';
export const useRecordBoardStates = (recordBoardId?: string) => {
const scopeId = useAvailableScopeIdOrThrow(
RecordBoardScopeInternalContext,
getScopeIdOrUndefinedFromComponentId(recordBoardId),
);
return {
scopeId,
getColumnIdsState: getState(recordBoardColumnIdsStateScopeMap, scopeId),
isFirstColumnFamilyState: getFamilyState(
isFirstRecordBoardColumnFamilyStateScopeMap,
scopeId,
),
isLastColumnFamilyState: getFamilyState(
isLastRecordBoardColumnFamilyStateScopeMap,
scopeId,
),
columnsFamilySelector: getFamilyState(
recordBoardColumnsFamilySelectorScopeMap,
scopeId,
),
};
};

View File

@ -0,0 +1,48 @@
import { useRecoilCallback } from 'recoil';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetRecordBoardColumns = (recordBoardId?: string) => {
const { scopeId, getColumnIdsState, columnsFamilySelector } =
useRecordBoardStates(recordBoardId);
const setRecordBoardColumns = useRecoilCallback(
({ set, snapshot }) =>
(columns: RecordBoardColumnDefinition[]) => {
const currentColumnsIds = snapshot
.getLoadable(getColumnIdsState())
.getValue();
const columnIds = columns.map(({ id }) => id);
if (isDeeplyEqual(currentColumnsIds, columnIds)) {
return;
}
set(
getColumnIdsState(),
columns.map((column) => column.id),
);
columns.forEach((column) => {
const currentColumn = snapshot
.getLoadable(columnsFamilySelector(column.id))
.getValue();
if (isDeeplyEqual(currentColumn, column)) {
return;
}
set(columnsFamilySelector(column.id), column);
});
},
[columnsFamilySelector, getColumnIdsState],
);
return {
scopeId,
setRecordBoardColumns,
};
};

View File

@ -0,0 +1,16 @@
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useSetRecordBoardColumns } from '@/object-record/record-board/hooks/internal/useSetRecordBoardColumns';
export const useRecordBoard = (recordBoardId?: string) => {
const { scopeId, getColumnIdsState, columnsFamilySelector } =
useRecordBoardStates(recordBoardId);
const { setRecordBoardColumns } = useSetRecordBoardColumns(recordBoardId);
return {
scopeId,
getColumnIdsState,
columnsFamilySelector,
setRecordBoardColumns,
};
};

View File

@ -1,10 +1,12 @@
import styled from '@emotion/styled';
import { Droppable } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil';
import { RecordBoardColumnContext } from '@/object-record/record-board/contexts/RecordBoardColumnContext';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer';
import { RecordBoardColumnHeader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeader';
import { BoardCardIdContext } from '@/object-record/record-board-deprecated/contexts/BoardCardIdContext';
import { BoardColumnDefinition } from '@/object-record/record-board-deprecated/types/BoardColumnDefinition';
const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
background-color: ${({ theme }) => theme.background.primary};
@ -22,25 +24,44 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
type RecordBoardColumnProps = {
recordBoardColumnId: string;
columnDefinition: BoardColumnDefinition;
};
export const RecordBoardColumn = ({
recordBoardColumnId,
columnDefinition,
}: RecordBoardColumnProps) => {
const isFirstColumn = columnDefinition.position === 0;
const {
isFirstColumnFamilyState,
isLastColumnFamilyState,
columnsFamilySelector,
} = useRecordBoardStates();
const columnDefinition = useRecoilValue(
columnsFamilySelector(recordBoardColumnId),
);
const isFirstColumn = useRecoilValue(
isFirstColumnFamilyState(recordBoardColumnId),
);
const isLastColumn = useRecoilValue(
isLastColumnFamilyState(recordBoardColumnId),
);
if (!columnDefinition) {
return null;
}
return (
<RecordBoardColumnContext.Provider
value={{
id: recordBoardColumnId,
columnDefinition: columnDefinition,
isFirstColumn: isFirstColumn,
isLastColumn: isLastColumn,
}}
>
<Droppable droppableId={recordBoardColumnId}>
{(droppableProvided) => (
<StyledColumn isFirstColumn={isFirstColumn}>
<RecordBoardColumnHeader />
<RecordBoardColumnCardsContainer
droppableProvided={droppableProvided}
>

View File

@ -0,0 +1,58 @@
import { useCallback, useContext, useRef } from 'react';
import styled from '@emotion/styled';
import { MenuItem } from 'tsup.ui.index';
import { RecordBoardColumnContext } from '@/object-record/record-board/contexts/RecordBoardColumnContext';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
const StyledMenuContainer = styled.div`
position: absolute;
top: ${({ theme }) => theme.spacing(10)};
width: 200px;
z-index: 1;
`;
type RecordBoardColumnDropdownMenuProps = {
onClose: () => void;
onDelete?: (id: string) => void;
stageId: string;
};
export const RecordBoardColumnDropdownMenu = ({
onClose,
}: RecordBoardColumnDropdownMenuProps) => {
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
const closeMenu = useCallback(() => {
onClose();
}, [onClose]);
useListenClickOutside({
refs: [boardColumnMenuRef],
callback: closeMenu,
});
const { columnDefinition } = useContext(RecordBoardColumnContext);
return (
<StyledMenuContainer ref={boardColumnMenuRef}>
<DropdownMenu data-select-disable>
<DropdownMenuItemsContainer>
{columnDefinition.actions.map((action) => (
<MenuItem
key={action.id}
onClick={() => {
action.callback();
closeMenu();
}}
LeftIcon={action.icon}
text={action.label}
/>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledMenuContainer>
);
};

View File

@ -0,0 +1,105 @@
import React, { useContext, useState } from 'react';
import styled from '@emotion/styled';
import { RecordBoardColumnContext } from '@/object-record/record-board/contexts/RecordBoardColumnContext';
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
import { BoardColumnHotkeyScope } from '@/object-record/record-board-deprecated/types/BoardColumnHotkeyScope';
import { IconDotsVertical } from '@/ui/display/icon';
import { Tag } from '@/ui/display/tag/components/Tag';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
const StyledHeader = styled.div`
align-items: center;
cursor: pointer;
display: flex;
flex-direction: row;
height: 24px;
justify-content: left;
margin-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledAmount = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
margin-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledNumChildren = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.border.radius.rounded};
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
height: 20px;
justify-content: center;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
margin-left: auto;
width: 16px;
`;
const StyledHeaderActions = styled.div`
display: flex;
margin-left: auto;
`;
export const RecordBoardColumnHeader = () => {
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
const { columnDefinition } = useContext(RecordBoardColumnContext);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const handleBoardColumnMenuOpen = () => {
setIsBoardColumnMenuOpen(true);
setHotkeyScopeAndMemorizePreviousScope(BoardColumnHotkeyScope.BoardColumn, {
goto: false,
});
};
const handleBoardColumnMenuClose = () => {
goBackToPreviousHotkeyScope();
setIsBoardColumnMenuOpen(false);
};
const boardColumnTotal = 0;
const cardIds = [];
return (
<>
<StyledHeader
onMouseEnter={() => setIsHeaderHovered(true)}
onMouseLeave={() => setIsHeaderHovered(false)}
>
<Tag
onClick={handleBoardColumnMenuOpen}
color={columnDefinition.color}
text={columnDefinition.title}
/>
{!!boardColumnTotal && <StyledAmount>${boardColumnTotal}</StyledAmount>}
{!isHeaderHovered && (
<StyledNumChildren>{cardIds.length}</StyledNumChildren>
)}
{isHeaderHovered && (
<StyledHeaderActions>
<LightIconButton
accent="tertiary"
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen}
/>
</StyledHeaderActions>
)}
</StyledHeader>
{isBoardColumnMenuOpen && (
<RecordBoardColumnDropdownMenu
onClose={handleBoardColumnMenuClose}
stageId={columnDefinition.id}
/>
)}
</>
);
};

View File

@ -0,0 +1,7 @@
import { createFamilyStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createFamilyStateScopeMap';
export const isFirstRecordBoardColumnFamilyStateScopeMap =
createFamilyStateScopeMap<boolean, string>({
key: 'isFirstRecordBoardColumnFamilyStateScopeMap',
defaultValue: false,
});

View File

@ -0,0 +1,7 @@
import { createFamilyStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createFamilyStateScopeMap';
export const isLastRecordBoardColumnFamilyStateScopeMap =
createFamilyStateScopeMap<boolean, string>({
key: 'isLastRecordBoardColumnFamilyStateScopeMap',
defaultValue: false,
});

View File

@ -0,0 +1,6 @@
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const recordBoardColumnIdsStateScopeMap = createStateScopeMap<string[]>({
key: 'recordBoardColumnIdsStateScopeMap',
defaultValue: [],
});

View File

@ -0,0 +1,10 @@
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { createFamilyStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createFamilyStateScopeMap';
export const recordBoardColumnsFamilyStateScopeMap = createFamilyStateScopeMap<
RecordBoardColumnDefinition | undefined,
string
>({
key: 'recordBoardColumnsFamilyStateScopeMap',
defaultValue: undefined,
});

View File

@ -0,0 +1,115 @@
import { isFirstRecordBoardColumnFamilyStateScopeMap } from '@/object-record/record-board/states/isFirstRecordBoardColumnFamilyStateScopeMap';
import { isLastRecordBoardColumnFamilyStateScopeMap } from '@/object-record/record-board/states/isLastRecordBoardColumnFamilyStateScopeMap';
import { recordBoardColumnIdsStateScopeMap } from '@/object-record/record-board/states/recordBoardColumnIdsStateScopeMap';
import { recordBoardColumnsFamilyStateScopeMap } from '@/object-record/record-board/states/recordBoardColumnsFamilyStateScopeMap';
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { createFamilySelectorScopeMap } from '@/ui/utilities/recoil-scope/utils/createFamilySelectorScopeMap';
import { guardRecoilDefaultValue } from '@/ui/utilities/recoil-scope/utils/guardRecoilDefaultValue';
import { assertNotNull } from '~/utils/assert';
export const recordBoardColumnsFamilySelectorScopeMap =
createFamilySelectorScopeMap<RecordBoardColumnDefinition | undefined, string>(
{
key: 'recordBoardColumnsFamilySelectorScopeMap',
get:
({
scopeId,
familyKey: columnId,
}: {
scopeId: string;
familyKey: string;
}) =>
({ get }) => {
return get(
recordBoardColumnsFamilyStateScopeMap({
scopeId,
familyKey: columnId,
}),
);
},
set:
({
scopeId,
familyKey: columnId,
}: {
scopeId: string;
familyKey: string;
}) =>
({ set, get }, newColumn) => {
set(
recordBoardColumnsFamilyStateScopeMap({
scopeId,
familyKey: columnId,
}),
newColumn,
);
if (guardRecoilDefaultValue(newColumn)) return;
const columnIds = get(recordBoardColumnIdsStateScopeMap({ scopeId }));
const columns = columnIds
.map((columnId) => {
return get(
recordBoardColumnsFamilyStateScopeMap({
scopeId,
familyKey: columnId,
}),
);
})
.filter(assertNotNull);
const lastColumn = [...columns].sort(
(a, b) => b.position - a.position,
)[0];
const firstColumn = [...columns].sort(
(a, b) => a.position - b.position,
)[0];
if (!newColumn) {
return;
}
if (!lastColumn || newColumn.position > lastColumn.position) {
set(
isLastRecordBoardColumnFamilyStateScopeMap({
scopeId,
familyKey: columnId,
}),
true,
);
if (lastColumn) {
set(
isLastRecordBoardColumnFamilyStateScopeMap({
scopeId,
familyKey: lastColumn.id,
}),
false,
);
}
}
if (!firstColumn || newColumn.position < firstColumn.position) {
set(
isFirstRecordBoardColumnFamilyStateScopeMap({
scopeId,
familyKey: columnId,
}),
true,
);
if (firstColumn) {
set(
isFirstRecordBoardColumnFamilyStateScopeMap({
scopeId,
familyKey: firstColumn.id,
}),
false,
);
}
}
},
},
);

View File

@ -0,0 +1,9 @@
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
export type RecordBoardColumnAction = {
id: string;
label: string;
icon: IconComponent;
position: number;
callback: () => void;
};

View File

@ -1,8 +1,10 @@
import { RecordBoardColumnAction } from '@/object-record/record-board/types/RecordBoardColumnAction';
import { ThemeColor } from '@/ui/theme/constants/colors';
export type RecordBoardColumnDefinition = {
id: string;
title: string;
position: number;
colorCode?: ThemeColor;
color: ThemeColor;
actions: RecordBoardColumnAction[];
};

View File

@ -1,11 +1,50 @@
import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
type RecordIndexBoardContainerEffectProps = {
objectNamePlural: string;
recordBoardId: string;
viewBarId: string;
};
export const RecordIndexBoardContainerEffect = (
_props: RecordIndexBoardContainerEffectProps,
) => {
export const RecordIndexBoardContainerEffect = ({
objectNamePlural,
recordBoardId,
}: RecordIndexBoardContainerEffectProps) => {
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const navigate = useNavigate();
const navigateToSelectSettings = useCallback(() => {
navigate(`/settings/objects/${objectNamePlural}`);
}, [navigate, objectNamePlural]);
const { setRecordBoardColumns } = useRecordBoard(recordBoardId);
useEffect(() => {
setRecordBoardColumns(
computeRecordBoardColumnDefinitionsFromObjectMetadata(
objectMetadataItem,
navigateToSelectSettings,
),
);
}, [
navigateToSelectSettings,
objectMetadataItem,
objectNameSingular,
setRecordBoardColumns,
]);
return <></>;
};

View File

@ -6,6 +6,7 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
import { RecordIndexBoardContainerEffect } from '@/object-record/record-index/components/RecordIndexBoardContainerEffect';
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
@ -129,9 +130,9 @@ export const RecordIndexContainer = ({
objectNamePlural={objectNamePlural}
createRecord={createRecord}
/>
<RecordIndexTableContainerEffect
<RecordIndexBoardContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
/>
</>

View File

@ -25,14 +25,14 @@ import { tableRowIdsStateScopeMap } from '@/object-record/record-table/states/ta
import { tableSortsStateScopeMap } from '@/object-record/record-table/states/tableSortsStateScopeMap';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getFamilyState } from '@/ui/utilities/recoil-scope/utils/getFamilyState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { getSelector } from '@/ui/utilities/recoil-scope/utils/getSelector';
import { getState } from '@/ui/utilities/recoil-scope/utils/getState';
export const useRecordTableStates = (recordTableId?: string) => {
const scopeId = useAvailableScopeIdOrThrow(
RecordTableScopeInternalContext,
getScopeIdFromComponentId(recordTableId),
getScopeIdOrUndefinedFromComponentId(recordTableId),
);
return {

View File

@ -0,0 +1,39 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
import { IconPencil } from '@/ui/display/icon';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const computeRecordBoardColumnDefinitionsFromObjectMetadata = (
objectMetadataItem: ObjectMetadataItem,
navigateToSelectSettings: () => void,
): RecordBoardColumnDefinition[] => {
const selectFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.type === FieldMetadataType.Select,
);
if (!selectFieldMetadataItem) {
return [];
}
if (!selectFieldMetadataItem.options) {
throw new Error(
`Select Field ${objectMetadataItem.nameSingular} has no options`,
);
}
return selectFieldMetadataItem.options.map((selectOption) => ({
id: selectOption.id,
title: selectOption.label,
color: selectOption.color,
position: selectOption.position,
actions: [
{
id: 'edit',
label: 'Edit from settings',
icon: IconPencil,
position: 0,
callback: navigateToSelectSettings,
},
],
}));
};

View File

@ -2,7 +2,7 @@ import { useRecoilState } from 'recoil';
import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdownStates';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
export const useDropdown = (dropdownId?: string) => {
const {
@ -11,7 +11,7 @@ export const useDropdown = (dropdownId?: string) => {
dropdownWidthState,
isDropdownOpenState,
} = useDropdownStates({
dropdownScopeId: getScopeIdFromComponentId(dropdownId),
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
});
const {

View File

@ -5,8 +5,7 @@ import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/get
import { getState } from '@/ui/utilities/recoil-scope/utils/getState';
export const useClickOustideListenerStates = (componentId: string) => {
// TODO: improve typing
const scopeId = getScopeIdFromComponentId(componentId) ?? '';
const scopeId = getScopeIdFromComponentId(componentId);
return {
scopeId,

View File

@ -1,2 +1,2 @@
export const getScopeIdFromComponentId = (componentId?: string) =>
componentId ? `${componentId}-scope` : undefined;
export const getScopeIdFromComponentId = (componentId: string) =>
`${componentId}-scope`;

View File

@ -0,0 +1,4 @@
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
export const getScopeIdOrUndefinedFromComponentId = (componentId?: string) =>
componentId ? getScopeIdFromComponentId(componentId) : undefined;

View File

@ -0,0 +1,8 @@
import { DefaultValue } from 'recoil';
export const guardRecoilDefaultValue = (
candidate: any,
): candidate is DefaultValue => {
if (candidate instanceof DefaultValue) return true;
return false;
};

View File

@ -16,6 +16,7 @@ export const seedOpportunity = async (
'amountCurrencyCode',
'closeDate',
'probability',
'stage',
'pipelineStepId',
'pointOfContactId',
'companyId',
@ -29,6 +30,7 @@ export const seedOpportunity = async (
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: 0.5,
stage: 'new',
pipelineStepId: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
pointOfContactId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
companyId: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
@ -40,6 +42,7 @@ export const seedOpportunity = async (
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: 0.5,
stage: 'meeting',
pipelineStepId: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
pointOfContactId: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
companyId: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
@ -51,6 +54,7 @@ export const seedOpportunity = async (
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: 0.5,
stage: 'proposal',
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
pointOfContactId: '9b324a88-6784-4449-afdf-dc62cb8702f2',
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
@ -62,6 +66,7 @@ export const seedOpportunity = async (
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: 0.5,
stage: 'proposal',
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
pointOfContactId: '98406e26-80f1-4dff-b570-a74942528de3',
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',

View File

@ -55,6 +55,27 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
})
probability: string;
@FieldMetadata({
type: FieldMetadataType.SELECT,
label: 'Stage',
description: 'Opportunity stage',
icon: 'IconProgressCheck',
options: [
{ value: 'new', label: 'New', position: 0, color: 'red' },
{ value: 'screening', label: 'Screening', position: 1, color: 'purple' },
{ value: 'meeting', label: 'Meeting', position: 2, color: 'sky' },
{
value: 'proposal',
label: 'Proposal',
position: 3,
color: 'turquoise',
},
{ value: 'customer', label: 'Customer', position: 4, color: 'yellow' },
],
defaultValue: { value: 'new' },
})
stage: string;
// Relations
@FieldMetadata({
type: FieldMetadataType.RELATION,