feat: view groups (#7176)
Fix #4244 and #4356 This pull request introduces the new "view groups" capability, enabling the reordering, hiding, and showing of columns in Kanban mode. The core enhancement includes the addition of a new entity named `ViewGroup`, which manages column behaviors and interactions. #### Key Changes: 1. **ViewGroup Entity**: The newly added `ViewGroup` entity is responsible for handling the organization and state of columns. This includes: - The ability to reorder columns. - The option to hide or show specific columns based on user preferences. #### Conclusion: This PR adds a significant new feature that enhances the flexibility of Kanban views through the `ViewGroup` entity. We'll later add the view group logic to table view too. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -31,16 +31,21 @@ const StyledContainer = styled.div`
|
||||
|
||||
const StyledColumnContainer = styled.div`
|
||||
display: flex;
|
||||
& > *:not(:first-child) {
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContainerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledBoardContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 48px);
|
||||
`;
|
||||
|
||||
const RecordBoardScrollRestoreEffect = () => {
|
||||
@ -137,6 +142,12 @@ export const RecordBoard = () => {
|
||||
],
|
||||
);
|
||||
|
||||
// FixMe: Check if we really need this as it depends on the times it takes to update the view groups
|
||||
// if (isPersistingViewGroups) {
|
||||
// // TODO: Add skeleton state
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return (
|
||||
<RecordBoardScope
|
||||
recordBoardScopeId={getScopeIdFromComponentId(recordBoardId)}
|
||||
|
||||
@ -17,6 +17,10 @@ const StyledHeaderContainer = styled.div`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
& > *:not(:first-child) {
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordBoardHeader = () => {
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
||||
import { isFirstRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState';
|
||||
import { isLastRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState';
|
||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
||||
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
||||
@ -51,14 +49,6 @@ export const useRecordBoardStates = (recordBoardId?: string) => {
|
||||
recordBoardColumnIdsComponentState,
|
||||
scopeId,
|
||||
),
|
||||
isFirstColumnFamilyState: extractComponentFamilyState(
|
||||
isFirstRecordBoardColumnComponentFamilyState,
|
||||
scopeId,
|
||||
),
|
||||
isLastColumnFamilyState: extractComponentFamilyState(
|
||||
isLastRecordBoardColumnComponentFamilyState,
|
||||
scopeId,
|
||||
),
|
||||
columnsFamilySelector: extractComponentFamilyState(
|
||||
recordBoardColumnsComponentFamilySelector,
|
||||
scopeId,
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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';
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
|
||||
export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
||||
const { scopeId, columnIdsState, columnsFamilySelector } =
|
||||
@ -10,21 +10,20 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
||||
|
||||
const setColumns = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(columns: RecordBoardColumnDefinition[]) => {
|
||||
(columns: RecordGroupDefinition[]) => {
|
||||
const currentColumnsIds = snapshot
|
||||
.getLoadable(columnIdsState)
|
||||
.getValue();
|
||||
|
||||
const columnIds = columns.map(({ id }) => id);
|
||||
const columnIds = columns
|
||||
.filter(({ isVisible }) => isVisible)
|
||||
.map(({ id }) => id);
|
||||
|
||||
if (isDeeplyEqual(currentColumnsIds, columnIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
columnIdsState,
|
||||
columns.map((column) => column.id),
|
||||
);
|
||||
set(columnIdsState, columnIds);
|
||||
|
||||
columns.forEach((column) => {
|
||||
const currentColumn = snapshot
|
||||
|
||||
@ -6,11 +6,8 @@ import { useRecordBoardStates } from '@/object-record/record-board/hooks/interna
|
||||
import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
|
||||
const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
|
||||
const StyledColumn = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-left: 1px solid
|
||||
${({ theme, isFirstColumn }) =>
|
||||
isFirstColumn ? 'none' : theme.border.color.light};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 200px;
|
||||
@ -32,24 +29,12 @@ type RecordBoardColumnProps = {
|
||||
export const RecordBoardColumn = ({
|
||||
recordBoardColumnId,
|
||||
}: RecordBoardColumnProps) => {
|
||||
const {
|
||||
isFirstColumnFamilyState,
|
||||
isLastColumnFamilyState,
|
||||
columnsFamilySelector,
|
||||
recordIdsByColumnIdFamilyState,
|
||||
} = useRecordBoardStates();
|
||||
const { columnsFamilySelector, recordIdsByColumnIdFamilyState } =
|
||||
useRecordBoardStates();
|
||||
const columnDefinition = useRecoilValue(
|
||||
columnsFamilySelector(recordBoardColumnId),
|
||||
);
|
||||
|
||||
const isFirstColumn = useRecoilValue(
|
||||
isFirstColumnFamilyState(recordBoardColumnId),
|
||||
);
|
||||
|
||||
const isLastColumn = useRecoilValue(
|
||||
isLastColumnFamilyState(recordBoardColumnId),
|
||||
);
|
||||
|
||||
const recordIds = useRecoilValue(
|
||||
recordIdsByColumnIdFamilyState(recordBoardColumnId),
|
||||
);
|
||||
@ -62,8 +47,6 @@ export const RecordBoardColumn = ({
|
||||
<RecordBoardColumnContext.Provider
|
||||
value={{
|
||||
columnDefinition: columnDefinition,
|
||||
isFirstColumn: isFirstColumn,
|
||||
isLastColumn: isLastColumn,
|
||||
recordCount: recordIds.length,
|
||||
columnId: recordBoardColumnId,
|
||||
recordIds,
|
||||
@ -71,7 +54,7 @@ export const RecordBoardColumn = ({
|
||||
>
|
||||
<Droppable droppableId={recordBoardColumnId}>
|
||||
{(droppableProvided) => (
|
||||
<StyledColumn isFirstColumn={isFirstColumn}>
|
||||
<StyledColumn>
|
||||
<RecordBoardColumnCardsContainer
|
||||
droppableProvided={droppableProvided}
|
||||
recordIds={recordIds}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useContext, useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
@ -25,6 +25,8 @@ export const RecordBoardColumnDropdownMenu = ({
|
||||
}: RecordBoardColumnDropdownMenuProps) => {
|
||||
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const recordGroupActions = useRecordGroupActions();
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
@ -34,13 +36,11 @@ export const RecordBoardColumnDropdownMenu = ({
|
||||
callback: closeMenu,
|
||||
});
|
||||
|
||||
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
||||
|
||||
return (
|
||||
<StyledMenuContainer ref={boardColumnMenuRef}>
|
||||
<DropdownMenu data-select-disable>
|
||||
<DropdownMenuItemsContainer>
|
||||
{columnDefinition.actions.map((action) => (
|
||||
{recordGroupActions.map((action) => (
|
||||
<MenuItem
|
||||
key={action.id}
|
||||
onClick={() => {
|
||||
|
||||
@ -10,7 +10,7 @@ import { RecordBoardColumnContext } from '@/object-record/record-board/record-bo
|
||||
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
|
||||
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
|
||||
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
|
||||
import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
|
||||
@ -59,11 +59,8 @@ const StyledRightContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
|
||||
const StyledColumn = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-left: 1px solid
|
||||
${({ theme, isFirstColumn }) =>
|
||||
isFirstColumn ? 'none' : theme.border.color.light};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 200px;
|
||||
@ -75,7 +72,7 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
|
||||
`;
|
||||
|
||||
export const RecordBoardColumnHeader = () => {
|
||||
const { columnDefinition, isFirstColumn, recordCount } = useContext(
|
||||
const { columnDefinition, recordCount } = useContext(
|
||||
RecordBoardColumnContext,
|
||||
);
|
||||
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
|
||||
@ -120,7 +117,7 @@ export const RecordBoardColumnHeader = () => {
|
||||
!isOpportunitiesCompanyFieldDisabled;
|
||||
|
||||
return (
|
||||
<StyledColumn isFirstColumn={isFirstColumn}>
|
||||
<StyledColumn>
|
||||
<StyledHeader
|
||||
onMouseEnter={() => setIsHeaderHovered(true)}
|
||||
onMouseLeave={() => setIsHeaderHovered(false)}
|
||||
@ -130,18 +127,18 @@ export const RecordBoardColumnHeader = () => {
|
||||
<Tag
|
||||
onClick={handleBoardColumnMenuOpen}
|
||||
variant={
|
||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? 'solid'
|
||||
: 'outline'
|
||||
}
|
||||
color={
|
||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? columnDefinition.color
|
||||
: 'transparent'
|
||||
}
|
||||
text={columnDefinition.title}
|
||||
weight={
|
||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? 'regular'
|
||||
: 'medium'
|
||||
}
|
||||
@ -154,13 +151,11 @@ export const RecordBoardColumnHeader = () => {
|
||||
<StyledRightContainer>
|
||||
{isHeaderHovered && (
|
||||
<StyledHeaderActions>
|
||||
{columnDefinition.actions.length > 0 && (
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconDotsVertical}
|
||||
onClick={handleBoardColumnMenuOpen}
|
||||
/>
|
||||
)}
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconDotsVertical}
|
||||
onClick={handleBoardColumnMenuOpen}
|
||||
/>
|
||||
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
@ -172,7 +167,7 @@ export const RecordBoardColumnHeader = () => {
|
||||
</StyledRightContainer>
|
||||
</StyledHeaderContainer>
|
||||
</StyledHeader>
|
||||
{isBoardColumnMenuOpen && columnDefinition.actions.length > 0 && (
|
||||
{isBoardColumnMenuOpen && (
|
||||
<RecordBoardColumnDropdownMenu
|
||||
onClose={handleBoardColumnMenuClose}
|
||||
stageId={columnDefinition.id}
|
||||
|
||||
@ -12,19 +12,11 @@ type RecordBoardColumnHeaderWrapperProps = {
|
||||
export const RecordBoardColumnHeaderWrapper = ({
|
||||
columnId,
|
||||
}: RecordBoardColumnHeaderWrapperProps) => {
|
||||
const {
|
||||
isFirstColumnFamilyState,
|
||||
isLastColumnFamilyState,
|
||||
columnsFamilySelector,
|
||||
recordIdsByColumnIdFamilyState,
|
||||
} = useRecordBoardStates();
|
||||
const { columnsFamilySelector, recordIdsByColumnIdFamilyState } =
|
||||
useRecordBoardStates();
|
||||
|
||||
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
|
||||
|
||||
const isFirstColumn = useRecoilValue(isFirstColumnFamilyState(columnId));
|
||||
|
||||
const isLastColumn = useRecoilValue(isLastColumnFamilyState(columnId));
|
||||
|
||||
const recordIds = useRecoilValue(recordIdsByColumnIdFamilyState(columnId));
|
||||
|
||||
if (!isDefined(columnDefinition)) {
|
||||
@ -36,8 +28,6 @@ export const RecordBoardColumnHeaderWrapper = ({
|
||||
value={{
|
||||
columnId,
|
||||
columnDefinition: columnDefinition,
|
||||
isFirstColumn: isFirstColumn,
|
||||
isLastColumn: isLastColumn,
|
||||
recordCount: recordIds.length,
|
||||
recordIds,
|
||||
}}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
|
||||
type RecordBoardColumnContextProps = {
|
||||
columnDefinition: RecordBoardColumnDefinition;
|
||||
isFirstColumn: boolean;
|
||||
isLastColumn: boolean;
|
||||
columnDefinition: RecordGroupDefinition;
|
||||
recordCount: number;
|
||||
columnId: string;
|
||||
recordIds: string[];
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
|
||||
type RecordBoardScopeProps = {
|
||||
children: ReactNode;
|
||||
recordBoardScopeId: string;
|
||||
onFieldsChange: (fields: FieldDefinition<FieldMetadata>[]) => void;
|
||||
onColumnsChange: (column: RecordBoardColumnDefinition[]) => void;
|
||||
onColumnsChange: (column: RecordGroupDefinition[]) => void;
|
||||
};
|
||||
|
||||
/** @deprecated */
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
|
||||
import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey';
|
||||
|
||||
type RecordBoardScopeInternalContextProps = RecoilComponentStateKey & {
|
||||
onFieldsChange: (fields: FieldDefinition<FieldMetadata>[]) => void;
|
||||
onColumnsChange: (column: RecordBoardColumnDefinition[]) => void;
|
||||
onColumnsChange: (column: RecordGroupDefinition[]) => void;
|
||||
};
|
||||
|
||||
export const RecordBoardScopeInternalContext =
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
|
||||
export const isFirstRecordBoardColumnComponentFamilyState =
|
||||
createComponentFamilyState<boolean, string>({
|
||||
key: 'isFirstRecordBoardColumnComponentFamilyState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
|
||||
export const isLastRecordBoardColumnComponentFamilyState =
|
||||
createComponentFamilyState<boolean, string>({
|
||||
key: 'isLastRecordBoardColumnComponentFamilyState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -1,8 +1,8 @@
|
||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
|
||||
export const recordBoardColumnsComponentFamilyState =
|
||||
createComponentFamilyState<RecordBoardColumnDefinition | undefined, string>({
|
||||
createComponentFamilyState<RecordGroupDefinition | undefined, string>({
|
||||
key: 'recordBoardColumnsComponentFamilyState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
|
||||
@ -1,19 +1,9 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { isFirstRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState';
|
||||
import { isLastRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState';
|
||||
import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState';
|
||||
import { recordBoardColumnsComponentFamilyState } from '@/object-record/record-board/states/recordBoardColumnsComponentFamilyState';
|
||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { guardRecoilDefaultValue } from '@/ui/utilities/recoil-scope/utils/guardRecoilDefaultValue';
|
||||
import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
|
||||
export const recordBoardColumnsComponentFamilySelector =
|
||||
createComponentFamilySelector<
|
||||
RecordBoardColumnDefinition | undefined,
|
||||
string
|
||||
>({
|
||||
createComponentFamilySelector<RecordGroupDefinition | undefined, string>({
|
||||
key: 'recordBoardColumnsComponentFamilySelector',
|
||||
get:
|
||||
({
|
||||
@ -39,7 +29,7 @@ export const recordBoardColumnsComponentFamilySelector =
|
||||
scopeId: string;
|
||||
familyKey: string;
|
||||
}) =>
|
||||
({ set, get }, newColumn) => {
|
||||
({ set }, newColumn) => {
|
||||
set(
|
||||
recordBoardColumnsComponentFamilyState({
|
||||
scopeId,
|
||||
@ -47,72 +37,5 @@ export const recordBoardColumnsComponentFamilySelector =
|
||||
}),
|
||||
newColumn,
|
||||
);
|
||||
|
||||
if (guardRecoilDefaultValue(newColumn)) return;
|
||||
|
||||
const columnIds = get(recordBoardColumnIdsComponentState({ scopeId }));
|
||||
|
||||
const columns = columnIds
|
||||
.map((columnId) => {
|
||||
return get(
|
||||
recordBoardColumnsComponentFamilyState({
|
||||
scopeId,
|
||||
familyKey: columnId,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
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(
|
||||
isLastRecordBoardColumnComponentFamilyState({
|
||||
scopeId,
|
||||
familyKey: columnId,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
if (!isUndefined(lastColumn)) {
|
||||
set(
|
||||
isLastRecordBoardColumnComponentFamilyState({
|
||||
scopeId,
|
||||
familyKey: lastColumn.id,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstColumn || newColumn.position < firstColumn.position) {
|
||||
set(
|
||||
isFirstRecordBoardColumnComponentFamilyState({
|
||||
scopeId,
|
||||
familyKey: columnId,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
if (!isUndefined(firstColumn)) {
|
||||
set(
|
||||
isFirstRecordBoardColumnComponentFamilyState({
|
||||
scopeId,
|
||||
familyKey: firstColumn.id,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import { ThemeColor } from 'twenty-ui';
|
||||
|
||||
import { RecordBoardColumnAction } from '@/object-record/record-board/types/RecordBoardColumnAction';
|
||||
|
||||
export const enum RecordBoardColumnDefinitionType {
|
||||
Value = 'value',
|
||||
NoValue = 'no-value',
|
||||
}
|
||||
|
||||
export type RecordBoardColumnDefinitionNoValue = {
|
||||
id: 'no-value';
|
||||
type: RecordBoardColumnDefinitionType.NoValue;
|
||||
title: 'No Value';
|
||||
position: number;
|
||||
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;
|
||||
@ -0,0 +1,96 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
|
||||
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
|
||||
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useRecordGroupActions = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const { objectNameSingular, recordIndexId } = useContext(
|
||||
RecordIndexRootPropsContext,
|
||||
);
|
||||
|
||||
const { columnDefinition: recordGroupDefinition } = useContext(
|
||||
RecordBoardColumnContext,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { viewGroupFieldMetadataItem } = useRecordGroups({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
|
||||
useRecordGroupVisibility({
|
||||
viewBarId: recordIndexId,
|
||||
});
|
||||
|
||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
const navigateToSelectSettings = useCallback(() => {
|
||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||
|
||||
if (!isDefined(viewGroupFieldMetadataItem)) {
|
||||
throw new Error('viewGroupFieldMetadataItem is not a non-empty string');
|
||||
}
|
||||
|
||||
const settingsPath = `/settings/objects/${getObjectSlug(objectMetadataItem)}/${getFieldSlug(viewGroupFieldMetadataItem)}`;
|
||||
|
||||
navigate(settingsPath);
|
||||
}, [
|
||||
setNavigationMemorizedUrl,
|
||||
location.pathname,
|
||||
location.search,
|
||||
navigate,
|
||||
objectMetadataItem,
|
||||
viewGroupFieldMetadataItem,
|
||||
]);
|
||||
|
||||
const recordGroupActions: RecordGroupAction[] = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
icon: IconSettings,
|
||||
position: 0,
|
||||
callback: () => {
|
||||
navigateToSelectSettings();
|
||||
},
|
||||
},
|
||||
recordGroupDefinition.type !== RecordGroupDefinitionType.NoValue
|
||||
? {
|
||||
id: 'hide',
|
||||
label: 'Hide',
|
||||
icon: IconEyeOff,
|
||||
position: 1,
|
||||
callback: () => {
|
||||
handleRecordGroupVisibilityChange(recordGroupDefinition);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
].filter(isDefined),
|
||||
[
|
||||
handleRecordGroupVisibilityChange,
|
||||
navigateToSelectSettings,
|
||||
recordGroupDefinition,
|
||||
],
|
||||
);
|
||||
|
||||
return recordGroupActions;
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
|
||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
|
||||
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type UseRecordGroupHandlersParams = {
|
||||
objectNameSingular: string;
|
||||
viewBarId: string;
|
||||
};
|
||||
|
||||
export const useRecordGroupReorder = ({
|
||||
objectNameSingular,
|
||||
viewBarId,
|
||||
}: UseRecordGroupHandlersParams) => {
|
||||
const setRecordGroupDefinitions = useSetRecoilComponentStateV2(
|
||||
recordGroupDefinitionsComponentState,
|
||||
);
|
||||
|
||||
const { visibleRecordGroups } = useRecordGroups({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
|
||||
|
||||
const handleOrderChange: OnDragEndResponder = useCallback(
|
||||
(result) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedVisibleBoardGroups = moveArrayItem(visibleRecordGroups, {
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
|
||||
if (isDeeplyEqual(visibleRecordGroups, reorderedVisibleBoardGroups))
|
||||
return;
|
||||
|
||||
const updatedGroups = [...reorderedVisibleBoardGroups].map(
|
||||
(group, index) => ({ ...group, position: index }),
|
||||
);
|
||||
|
||||
setRecordGroupDefinitions(updatedGroups);
|
||||
saveViewGroups(mapRecordGroupDefinitionsToViewGroups(updatedGroups));
|
||||
},
|
||||
[saveViewGroups, setRecordGroupDefinitions, visibleRecordGroups],
|
||||
);
|
||||
|
||||
return {
|
||||
visibleRecordGroups,
|
||||
handleOrderChange,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
|
||||
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
|
||||
|
||||
type UseRecordGroupVisibilityParams = {
|
||||
viewBarId: string;
|
||||
};
|
||||
|
||||
export const useRecordGroupVisibility = ({
|
||||
viewBarId,
|
||||
}: UseRecordGroupVisibilityParams) => {
|
||||
const [recordGroupDefinitions, setRecordGroupDefinitions] =
|
||||
useRecoilComponentStateV2(recordGroupDefinitionsComponentState);
|
||||
|
||||
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
|
||||
|
||||
const handleVisibilityChange = useCallback(
|
||||
async (updatedRecordGroupDefinition: RecordGroupDefinition) => {
|
||||
const updatedRecordGroupDefinitions = recordGroupDefinitions.map(
|
||||
(groupDefinition) =>
|
||||
groupDefinition.id === updatedRecordGroupDefinition.id
|
||||
? {
|
||||
...groupDefinition,
|
||||
isVisible: !groupDefinition.isVisible,
|
||||
}
|
||||
: groupDefinition,
|
||||
);
|
||||
|
||||
setRecordGroupDefinitions(updatedRecordGroupDefinitions);
|
||||
|
||||
saveViewGroups(
|
||||
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions),
|
||||
);
|
||||
},
|
||||
[recordGroupDefinitions, setRecordGroupDefinitions, saveViewGroups],
|
||||
);
|
||||
|
||||
return {
|
||||
handleVisibilityChange,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,58 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
type UseRecordGroupsParams = {
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const useRecordGroups = ({
|
||||
objectNameSingular,
|
||||
}: UseRecordGroupsParams) => {
|
||||
const recordGroupDefinitions = useRecoilComponentValueV2(
|
||||
recordGroupDefinitionsComponentState,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const viewGroupFieldMetadataItem = useMemo(() => {
|
||||
if (recordGroupDefinitions.length === 0) return null;
|
||||
// We're assuming that all groups have the same fieldMetadataId for now
|
||||
const fieldMetadataId =
|
||||
'fieldMetadataId' in recordGroupDefinitions[0]
|
||||
? recordGroupDefinitions[0].fieldMetadataId
|
||||
: null;
|
||||
|
||||
if (!fieldMetadataId) return null;
|
||||
|
||||
return objectMetadataItem.fields.find(
|
||||
(field) => field.id === fieldMetadataId,
|
||||
);
|
||||
}, [objectMetadataItem, recordGroupDefinitions]);
|
||||
|
||||
const visibleRecordGroups = useMemo(
|
||||
() =>
|
||||
recordGroupDefinitions
|
||||
.filter((boardGroup) => boardGroup.isVisible)
|
||||
.sort(
|
||||
(boardGroupA, boardGroupB) =>
|
||||
boardGroupA.position - boardGroupB.position,
|
||||
),
|
||||
[recordGroupDefinitions],
|
||||
);
|
||||
|
||||
const hiddenRecordGroups = useMemo(
|
||||
() => recordGroupDefinitions.filter((boardGroup) => !boardGroup.isVisible),
|
||||
[recordGroupDefinitions],
|
||||
);
|
||||
|
||||
return {
|
||||
hiddenRecordGroups,
|
||||
visibleRecordGroups,
|
||||
viewGroupFieldMetadataItem,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const recordGroupDefinitionsComponentState = createComponentStateV2<
|
||||
RecordGroupDefinition[]
|
||||
>({
|
||||
key: 'recordGroupDefinitionsComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
export type RecordBoardColumnAction = {
|
||||
export type RecordGroupAction = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: IconComponent;
|
||||
@ -0,0 +1,17 @@
|
||||
import { ThemeColor } from 'twenty-ui';
|
||||
|
||||
export const enum RecordGroupDefinitionType {
|
||||
Value = 'value',
|
||||
NoValue = 'no-value',
|
||||
}
|
||||
|
||||
export type RecordGroupDefinition = {
|
||||
id: string;
|
||||
fieldMetadataId: string;
|
||||
type: RecordGroupDefinitionType;
|
||||
title: string;
|
||||
value: string | null;
|
||||
color: ThemeColor | 'transparent';
|
||||
position: number;
|
||||
isVisible: boolean;
|
||||
};
|
||||
@ -9,14 +9,12 @@ import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/get
|
||||
|
||||
export const RecordIndexBoardColumnLoaderEffect = ({
|
||||
objectNameSingular,
|
||||
boardFieldSelectValue,
|
||||
boardFieldMetadataId,
|
||||
recordBoardId,
|
||||
columnId,
|
||||
}: {
|
||||
recordBoardId: string;
|
||||
objectNameSingular: string;
|
||||
boardFieldSelectValue: string | null;
|
||||
boardFieldMetadataId: string | null;
|
||||
columnId: string;
|
||||
}) => {
|
||||
@ -40,7 +38,6 @@ export const RecordIndexBoardColumnLoaderEffect = ({
|
||||
objectNameSingular,
|
||||
recordBoardId,
|
||||
boardFieldMetadataId,
|
||||
columnFieldSelectValue: boardFieldSelectValue,
|
||||
columnId,
|
||||
});
|
||||
|
||||
@ -70,7 +67,6 @@ export const RecordIndexBoardColumnLoaderEffect = ({
|
||||
fetchMoreRecords,
|
||||
loading,
|
||||
shouldFetchMore,
|
||||
boardFieldSelectValue,
|
||||
setLoadingRecordsForThisColumn,
|
||||
loadingRecordsForThisColumn,
|
||||
|
||||
|
||||
@ -26,23 +26,18 @@ export const RecordIndexBoardDataLoader = ({
|
||||
(field) => field.id === recordIndexKanbanFieldMetadataId,
|
||||
);
|
||||
|
||||
const possibleKanbanSelectFieldValues =
|
||||
recordIndexKanbanFieldMetadataItem?.options ?? [];
|
||||
|
||||
const { columnIdsState } = useRecordBoardStates(recordBoardId);
|
||||
|
||||
// TODO: we should make sure there's no way to have a mismatch between columnIds and possibleKanbanSelectFieldValues order
|
||||
const columnIds = useRecoilValue(columnIdsState);
|
||||
|
||||
return (
|
||||
<>
|
||||
{possibleKanbanSelectFieldValues.map((option, index) => (
|
||||
{columnIds.map((columnId, index) => (
|
||||
<RecordIndexBoardColumnLoaderEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
boardFieldMetadataId={recordIndexKanbanFieldMetadataId}
|
||||
boardFieldSelectValue={option.value}
|
||||
recordBoardId={recordBoardId}
|
||||
columnId={columnIds[index]}
|
||||
columnId={columnId}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
@ -50,7 +45,6 @@ export const RecordIndexBoardDataLoader = ({
|
||||
<RecordIndexBoardColumnLoaderEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
boardFieldMetadataId={recordIndexKanbanFieldMetadataId}
|
||||
boardFieldSelectValue={null}
|
||||
recordBoardId={recordBoardId}
|
||||
columnId={'no-value'}
|
||||
/>
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -32,6 +30,10 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
||||
recordIndexFieldDefinitionsState,
|
||||
);
|
||||
|
||||
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
||||
recordGroupDefinitionsComponentState,
|
||||
);
|
||||
|
||||
const recordIndexKanbanFieldMetadataId = useRecoilValue(
|
||||
recordIndexKanbanFieldMetadataIdState,
|
||||
);
|
||||
@ -60,43 +62,17 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
||||
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
const navigateToSelectSettings = useCallback(() => {
|
||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||
navigate(`/settings/objects/${getObjectSlug(objectMetadataItem)}`);
|
||||
}, [
|
||||
navigate,
|
||||
objectMetadataItem,
|
||||
location.pathname,
|
||||
location.search,
|
||||
setNavigationMemorizedUrl,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setObjectSingularName(objectNameSingular);
|
||||
}, [objectNameSingular, setObjectSingularName]);
|
||||
|
||||
useEffect(() => {
|
||||
setColumns(
|
||||
computeRecordBoardColumnDefinitionsFromObjectMetadata(
|
||||
objectMetadataItem,
|
||||
recordIndexKanbanFieldMetadataId ?? '',
|
||||
navigateToSelectSettings,
|
||||
),
|
||||
);
|
||||
}, [
|
||||
navigateToSelectSettings,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
recordIndexKanbanFieldMetadataId,
|
||||
setColumns,
|
||||
]);
|
||||
setColumns(recordIndexGroupDefinitions);
|
||||
}, [recordIndexGroupDefinitions, setColumns]);
|
||||
|
||||
// TODO: Remove this duplicate useEffect by ensuring it's not here because
|
||||
// We want it to be triggered by a change of objectMetadataItem, which would be an anti-pattern
|
||||
// As it is an unnecessary dependency
|
||||
useEffect(() => {
|
||||
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||
}, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]);
|
||||
|
||||
@ -24,13 +24,17 @@ import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/compone
|
||||
|
||||
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions';
|
||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
import { useContext } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
@ -61,6 +65,10 @@ export const RecordIndexContainer = () => {
|
||||
objectNameSingular,
|
||||
} = useContext(RecordIndexRootPropsContext);
|
||||
|
||||
const recordGroupDefinitionsCallbackState = useRecoilComponentCallbackStateV2(
|
||||
recordGroupDefinitionsComponentState,
|
||||
);
|
||||
|
||||
const { columnDefinitions, filterDefinitions, sortDefinitions } =
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
@ -77,6 +85,8 @@ export const RecordIndexContainer = () => {
|
||||
recordTableId: recordIndexId,
|
||||
});
|
||||
|
||||
const { setColumns } = useRecordBoard(recordIndexId);
|
||||
|
||||
const onViewFieldsChange = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(viewFields: ViewField[]) => {
|
||||
@ -103,6 +113,32 @@ export const RecordIndexContainer = () => {
|
||||
[columnDefinitions, setTableColumns],
|
||||
);
|
||||
|
||||
const onViewGroupsChange = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(viewGroups: ViewGroup[]) => {
|
||||
const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({
|
||||
objectMetadataItem,
|
||||
viewGroups,
|
||||
});
|
||||
|
||||
setColumns(newGroupDefinitions);
|
||||
|
||||
const existingRecordIndexGroupDefinitions = snapshot
|
||||
.getLoadable(recordGroupDefinitionsCallbackState)
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
existingRecordIndexGroupDefinitions,
|
||||
newGroupDefinitions,
|
||||
)
|
||||
) {
|
||||
set(recordGroupDefinitionsCallbackState, newGroupDefinitions);
|
||||
}
|
||||
},
|
||||
[objectMetadataItem, recordGroupDefinitionsCallbackState, setColumns],
|
||||
);
|
||||
|
||||
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
@ -110,86 +146,83 @@ export const RecordIndexContainer = () => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<InformationBannerWrapper />
|
||||
<ViewComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordIndexId }}
|
||||
>
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<SpreadsheetImportProvider>
|
||||
<ViewBar
|
||||
viewBarId={recordIndexId}
|
||||
optionsDropdownButton={
|
||||
<RecordIndexOptionsDropdown
|
||||
recordIndexId={recordIndexId}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
viewType={recordIndexViewType ?? ViewType.Table}
|
||||
/>
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<SpreadsheetImportProvider>
|
||||
<ViewBar
|
||||
viewBarId={recordIndexId}
|
||||
optionsDropdownButton={
|
||||
<RecordIndexOptionsDropdown
|
||||
recordIndexId={recordIndexId}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
viewType={recordIndexViewType ?? ViewType.Table}
|
||||
/>
|
||||
}
|
||||
onCurrentViewChange={(view) => {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
onCurrentViewChange={(view) => {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
onViewFieldsChange(view.viewFields);
|
||||
setTableFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setContextStoreTargetedRecordsRule((prev) => ({
|
||||
...prev,
|
||||
filters: mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterDefinitions,
|
||||
),
|
||||
}));
|
||||
setTableSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexViewType(view.type);
|
||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||
view.kanbanFieldMetadataId,
|
||||
);
|
||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||
}}
|
||||
/>
|
||||
<RecordIndexViewBarEffect
|
||||
objectNamePlural={objectNamePlural}
|
||||
onViewFieldsChange(view.viewFields);
|
||||
onViewGroupsChange(view.viewGroups);
|
||||
setTableFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setContextStoreTargetedRecordsRule((prev) => ({
|
||||
...prev,
|
||||
filters: mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterDefinitions,
|
||||
),
|
||||
}));
|
||||
setTableSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexViewType(view.type);
|
||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||
view.kanbanFieldMetadataId,
|
||||
);
|
||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||
}}
|
||||
/>
|
||||
<RecordIndexViewBarEffect
|
||||
objectNamePlural={objectNamePlural}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
</SpreadsheetImportProvider>
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
<>
|
||||
<RecordIndexTableContainer
|
||||
recordTableId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
</SpreadsheetImportProvider>
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
<>
|
||||
<RecordIndexTableContainer
|
||||
recordTableId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexTableContainerEffect />
|
||||
</>
|
||||
)}
|
||||
{recordIndexViewType === ViewType.Kanban && (
|
||||
<StyledContainerWithPadding>
|
||||
<RecordIndexBoardContainer
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
<RecordIndexBoardDataLoader
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexBoardDataLoaderEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
/>
|
||||
</StyledContainerWithPadding>
|
||||
)}
|
||||
<RecordIndexActionMenu actionMenuId={recordIndexId} />
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
<RecordIndexTableContainerEffect />
|
||||
</>
|
||||
)}
|
||||
{recordIndexViewType === ViewType.Kanban && (
|
||||
<StyledContainerWithPadding>
|
||||
<RecordIndexBoardContainer
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
<RecordIndexBoardDataLoader
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexBoardDataLoaderEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
/>
|
||||
</StyledContainerWithPadding>
|
||||
)}
|
||||
<RecordIndexActionMenu actionMenuId={recordIndexId} />
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
|
||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
|
||||
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||
@ -58,7 +58,7 @@ export const RecordIndexPageKanbanAddButton = () => {
|
||||
const { handleAddNewCardClick } = useAddNewCard();
|
||||
|
||||
const handleItemClick = useCallback(
|
||||
(columnDefinition: RecordBoardColumnDefinition) => {
|
||||
(columnDefinition: RecordGroupDefinition) => {
|
||||
const isOpportunityEnabled =
|
||||
isOpportunity && !isOpportunitiesCompanyFieldDisabled;
|
||||
handleAddNewCardClick(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { useRecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import styled from '@emotion/styled';
|
||||
@ -32,18 +32,18 @@ export const RecordIndexPageKanbanAddMenuItem = ({
|
||||
text={
|
||||
<Tag
|
||||
variant={
|
||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? 'solid'
|
||||
: 'outline'
|
||||
}
|
||||
color={
|
||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? columnDefinition.color
|
||||
: 'transparent'
|
||||
}
|
||||
text={columnDefinition.title}
|
||||
weight={
|
||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
||||
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||
? 'regular'
|
||||
: 'medium'
|
||||
}
|
||||
|
||||
@ -6,12 +6,14 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
|
||||
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
||||
|
||||
type UseLoadRecordIndexBoardProps = {
|
||||
@ -31,6 +33,7 @@ export const useLoadRecordIndexBoard = ({
|
||||
const {
|
||||
setRecordIds: setRecordIdsInBoard,
|
||||
setFieldDefinitions,
|
||||
setColumns,
|
||||
isCompactModeActiveState,
|
||||
} = useRecordBoard(recordBoardId);
|
||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
@ -42,6 +45,13 @@ export const useLoadRecordIndexBoard = ({
|
||||
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
||||
|
||||
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
||||
recordGroupDefinitionsComponentState,
|
||||
);
|
||||
useEffect(() => {
|
||||
setColumns(recordIndexGroupDefinitions);
|
||||
}, [recordIndexGroupDefinitions, setColumns]);
|
||||
|
||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||
const requestFilters = turnFiltersIntoQueryFilter(
|
||||
|
||||
@ -11,12 +11,12 @@ import { recordIndexFiltersState } from '@/object-record/record-index/states/rec
|
||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||
|
||||
type UseLoadRecordIndexBoardProps = {
|
||||
objectNameSingular: string;
|
||||
boardFieldMetadataId: string | null;
|
||||
recordBoardId: string;
|
||||
columnFieldSelectValue: string | null;
|
||||
columnId: string;
|
||||
};
|
||||
|
||||
@ -24,17 +24,18 @@ export const useLoadRecordIndexBoardColumn = ({
|
||||
objectNameSingular,
|
||||
boardFieldMetadataId,
|
||||
recordBoardId,
|
||||
columnFieldSelectValue,
|
||||
columnId,
|
||||
}: UseLoadRecordIndexBoardProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { setRecordIdsForColumn } = useRecordBoard(recordBoardId);
|
||||
const { columnsFamilySelector } = useRecordBoardStates(recordBoardId);
|
||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
|
||||
const requestFilters = turnFiltersIntoQueryFilter(
|
||||
recordIndexFilters,
|
||||
objectMetadataItem?.fields ?? [],
|
||||
@ -53,9 +54,9 @@ export const useLoadRecordIndexBoardColumn = ({
|
||||
const filter = {
|
||||
...requestFilters,
|
||||
[recordIndexKanbanFieldMetadataItem?.name ?? '']: isDefined(
|
||||
columnFieldSelectValue,
|
||||
columnDefinition?.value,
|
||||
)
|
||||
? { in: [columnFieldSelectValue] }
|
||||
? { in: [columnDefinition?.value] }
|
||||
: { is: 'NULL' },
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import {
|
||||
IconBaselineDensitySmall,
|
||||
@ -10,6 +10,7 @@ import {
|
||||
IconSettings,
|
||||
IconTag,
|
||||
UndecoratedLink,
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
|
||||
@ -21,6 +22,9 @@ import {
|
||||
useExportRecordData,
|
||||
} from '@/action-menu/hooks/useExportRecordData';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder';
|
||||
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
|
||||
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
|
||||
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
|
||||
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
|
||||
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
||||
@ -37,12 +41,17 @@ import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemTog
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
||||
import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
type RecordIndexOptionsMenu = 'fields' | 'hiddenFields';
|
||||
type RecordIndexOptionsMenu =
|
||||
| 'viewGroups'
|
||||
| 'hiddenViewGroups'
|
||||
| 'fields'
|
||||
| 'hiddenFields';
|
||||
|
||||
type RecordIndexOptionsDropdownContentProps = {
|
||||
recordIndexId: string;
|
||||
@ -50,6 +59,7 @@ type RecordIndexOptionsDropdownContentProps = {
|
||||
viewType: ViewType;
|
||||
};
|
||||
|
||||
// TODO: Break this component down
|
||||
export const RecordIndexOptionsDropdownContent = ({
|
||||
viewType,
|
||||
recordIndexId,
|
||||
@ -57,6 +67,8 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
}: RecordIndexOptionsDropdownContentProps) => {
|
||||
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
|
||||
|
||||
const [currentMenu, setCurrentMenu] = useState<
|
||||
@ -111,6 +123,28 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
viewBarId: recordIndexId,
|
||||
});
|
||||
|
||||
const {
|
||||
hiddenRecordGroups,
|
||||
visibleRecordGroups,
|
||||
viewGroupFieldMetadataItem,
|
||||
} = useRecordGroups({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
|
||||
useRecordGroupVisibility({
|
||||
viewBarId: recordIndexId,
|
||||
});
|
||||
const { handleOrderChange: handleRecordGroupOrderChange } =
|
||||
useRecordGroupReorder({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
viewBarId: recordIndexId,
|
||||
});
|
||||
|
||||
const viewGroupSettingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, {
|
||||
id: viewGroupFieldMetadataItem?.name,
|
||||
objectSlug: objectNamePlural,
|
||||
});
|
||||
|
||||
const visibleRecordFields =
|
||||
viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns;
|
||||
|
||||
@ -143,10 +177,28 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
const isViewGroupMenuItemVisible =
|
||||
viewGroupFieldMetadataItem &&
|
||||
(visibleRecordGroups.length > 0 || hiddenRecordGroups.length > 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentMenu === 'hiddenViewGroups' && hiddenRecordGroups.length === 0) {
|
||||
setCurrentMenu('viewGroups');
|
||||
}
|
||||
}, [hiddenRecordGroups, currentMenu]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!currentMenu && (
|
||||
<DropdownMenuItemsContainer>
|
||||
{isViewGroupMenuItemVisible && (
|
||||
<MenuItem
|
||||
onClick={() => handleSelectMenu('viewGroups')}
|
||||
LeftIcon={getIcon(currentViewWithCombinedFiltersAndSorts?.icon)}
|
||||
text={viewGroupFieldMetadataItem.label}
|
||||
hasSubMenu
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => handleSelectMenu('fields')}
|
||||
LeftIcon={IconTag}
|
||||
@ -174,6 +226,34 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
{currentMenu === 'viewGroups' && (
|
||||
<>
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
||||
{viewGroupFieldMetadataItem?.label}
|
||||
</DropdownMenuHeader>
|
||||
<ViewGroupsVisibilityDropdownSection
|
||||
title={viewGroupFieldMetadataItem?.label ?? ''}
|
||||
viewGroups={visibleRecordGroups}
|
||||
onDragEnd={handleRecordGroupOrderChange}
|
||||
onVisibilityChange={handleRecordGroupVisibilityChange}
|
||||
isDraggable
|
||||
showSubheader={false}
|
||||
showDragGrip={true}
|
||||
/>
|
||||
{hiddenRecordGroups.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemNavigate
|
||||
onClick={() => handleSelectMenu('hiddenViewGroups')}
|
||||
LeftIcon={IconEyeOff}
|
||||
text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentMenu === 'fields' && (
|
||||
<>
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
||||
@ -198,6 +278,36 @@ export const RecordIndexOptionsDropdownContent = ({
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
{currentMenu === 'hiddenViewGroups' && (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
StartIcon={IconChevronLeft}
|
||||
onClick={() => setCurrentMenu('viewGroups')}
|
||||
>
|
||||
Hidden {viewGroupFieldMetadataItem?.label}
|
||||
</DropdownMenuHeader>
|
||||
<ViewGroupsVisibilityDropdownSection
|
||||
title={`Hidden ${viewGroupFieldMetadataItem?.label}`}
|
||||
viewGroups={hiddenRecordGroups}
|
||||
onVisibilityChange={handleRecordGroupVisibilityChange}
|
||||
isDraggable={false}
|
||||
showSubheader={false}
|
||||
showDragGrip={false}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<UndecoratedLink
|
||||
to={viewGroupSettingsUrl}
|
||||
onClick={() => {
|
||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||
closeDropdown();
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem LeftIcon={IconSettings} text="Edit field values" />
|
||||
</DropdownMenuItemsContainer>
|
||||
</UndecoratedLink>
|
||||
</>
|
||||
)}
|
||||
{currentMenu === 'hiddenFields' && (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import { expect } from '@storybook/test';
|
||||
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '../computeRecordBoardColumnDefinitionsFromObjectMetadata';
|
||||
|
||||
describe('computeRecordBoardColumnDefinitionsFromObjectMetadata', () => {
|
||||
it('should correctly compute', () => {
|
||||
const objectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'opportunity',
|
||||
);
|
||||
|
||||
const stageField = objectMetadataItem?.fields.find(
|
||||
(field) => field.name === 'stage',
|
||||
);
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
throw new Error('Object metadata item not found');
|
||||
}
|
||||
|
||||
const res = computeRecordBoardColumnDefinitionsFromObjectMetadata(
|
||||
objectMetadataItem,
|
||||
stageField?.id,
|
||||
() => null,
|
||||
);
|
||||
expect(res.length).toEqual(stageField?.options?.length);
|
||||
});
|
||||
});
|
||||
@ -1,71 +0,0 @@
|
||||
import { IconSettings } from 'twenty-ui';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
RecordBoardColumnDefinition,
|
||||
RecordBoardColumnDefinitionNoValue,
|
||||
RecordBoardColumnDefinitionType,
|
||||
RecordBoardColumnDefinitionValue,
|
||||
} from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const computeRecordBoardColumnDefinitionsFromObjectMetadata = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
kanbanFieldMetadataId: string,
|
||||
navigateToSelectSettings: () => void,
|
||||
): RecordBoardColumnDefinition[] => {
|
||||
const selectFieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.id === kanbanFieldMetadataId &&
|
||||
field.type === FieldMetadataType.Select,
|
||||
);
|
||||
|
||||
if (!selectFieldMetadataItem) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!selectFieldMetadataItem.options) {
|
||||
throw new Error(
|
||||
`Select Field ${objectMetadataItem.nameSingular} has no options`,
|
||||
);
|
||||
}
|
||||
|
||||
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: IconSettings,
|
||||
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