mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 23:37:21 +03:00
Display columns on Record Board (#3626)
* Display columns on Record board * Fix * Fix according to review * Fix
This commit is contained in:
parent
ca6250286a
commit
377fd23c90
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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}
|
||||
>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import { createFamilyStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createFamilyStateScopeMap';
|
||||
|
||||
export const isFirstRecordBoardColumnFamilyStateScopeMap =
|
||||
createFamilyStateScopeMap<boolean, string>({
|
||||
key: 'isFirstRecordBoardColumnFamilyStateScopeMap',
|
||||
defaultValue: false,
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
import { createFamilyStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createFamilyStateScopeMap';
|
||||
|
||||
export const isLastRecordBoardColumnFamilyStateScopeMap =
|
||||
createFamilyStateScopeMap<boolean, string>({
|
||||
key: 'isLastRecordBoardColumnFamilyStateScopeMap',
|
||||
defaultValue: false,
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
|
||||
|
||||
export const recordBoardColumnIdsStateScopeMap = createStateScopeMap<string[]>({
|
||||
key: 'recordBoardColumnIdsStateScopeMap',
|
||||
defaultValue: [],
|
||||
});
|
@ -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,
|
||||
});
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
@ -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;
|
||||
};
|
@ -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[];
|
||||
};
|
||||
|
@ -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 <></>;
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -1,2 +1,2 @@
|
||||
export const getScopeIdFromComponentId = (componentId?: string) =>
|
||||
componentId ? `${componentId}-scope` : undefined;
|
||||
export const getScopeIdFromComponentId = (componentId: string) =>
|
||||
`${componentId}-scope`;
|
||||
|
@ -0,0 +1,4 @@
|
||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||
|
||||
export const getScopeIdOrUndefinedFromComponentId = (componentId?: string) =>
|
||||
componentId ? getScopeIdFromComponentId(componentId) : undefined;
|
@ -0,0 +1,8 @@
|
||||
import { DefaultValue } from 'recoil';
|
||||
|
||||
export const guardRecoilDefaultValue = (
|
||||
candidate: any,
|
||||
): candidate is DefaultValue => {
|
||||
if (candidate instanceof DefaultValue) return true;
|
||||
return false;
|
||||
};
|
@ -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',
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user