Skip to content

Commit

Permalink
feat: view groups (#7176)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
magrinj and lucasbordeau authored Oct 24, 2024
1 parent 68a060a commit e8d96cf
Show file tree
Hide file tree
Showing 61 changed files with 1,408 additions and 508 deletions.
1 change: 1 addition & 0 deletions packages/twenty-front/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ const config: StorybookConfig = {
},
});
},
logLevel: 'error',
};
export default config;
1 change: 1 addition & 0 deletions packages/twenty-front/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ initialize({
with payload ${JSON.stringify(requestBody)}\n
This request should be mocked with MSW`);
},
quiet: true,
});

const preview: Preview = {
Expand Down
2 changes: 1 addition & 1 deletion packages/twenty-front/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const jestConfig: JestConfigWithTsJest = {
global: {
statements: 59,
lines: 55,
functions: 49,
functions: 48,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum CoreObjectNameSingular {
ViewField = 'viewField',
ViewFilter = 'viewFilter',
ViewSort = 'viewSort',
ViewGroup = 'viewGroup',
Webhook = 'webhook',
WorkspaceMember = 'workspaceMember',
MessageThreadSubscriber = 'messageThreadSubscriber',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -51,14 +49,6 @@ export const useRecordBoardStates = (recordBoardId?: string) => {
recordBoardColumnIdsComponentState,
scopeId,
),
isFirstColumnFamilyState: extractComponentFamilyState(
isFirstRecordBoardColumnComponentFamilyState,
scopeId,
),
isLastColumnFamilyState: extractComponentFamilyState(
isLastRecordBoardColumnComponentFamilyState,
scopeId,
),
columnsFamilySelector: extractComponentFamilyState(
recordBoardColumnsComponentFamilySelector,
scopeId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
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 } =
useRecordBoardStates(recordBoardId);

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
);
Expand All @@ -62,16 +47,14 @@ export const RecordBoardColumn = ({
<RecordBoardColumnContext.Provider
value={{
columnDefinition: columnDefinition,
isFirstColumn: isFirstColumn,
isLastColumn: isLastColumn,
recordCount: recordIds.length,
columnId: recordBoardColumnId,
recordIds,
}}
>
<Droppable droppableId={recordBoardColumnId}>
{(droppableProvided) => (
<StyledColumn isFirstColumn={isFirstColumn}>
<StyledColumn>
<RecordBoardColumnCardsContainer
droppableProvided={droppableProvided}
recordIds={recordIds}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,6 +25,8 @@ export const RecordBoardColumnDropdownMenu = ({
}: RecordBoardColumnDropdownMenuProps) => {
const boardColumnMenuRef = useRef<HTMLDivElement>(null);

const recordGroupActions = useRecordGroupActions();

const closeMenu = useCallback(() => {
onClose();
}, [onClose]);
Expand All @@ -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={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -120,7 +117,7 @@ export const RecordBoardColumnHeader = () => {
!isOpportunitiesCompanyFieldDisabled;

return (
<StyledColumn isFirstColumn={isFirstColumn}>
<StyledColumn>
<StyledHeader
onMouseEnter={() => setIsHeaderHovered(true)}
onMouseLeave={() => setIsHeaderHovered(false)}
Expand All @@ -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'
}
Expand All @@ -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"
Expand All @@ -172,7 +167,7 @@ export const RecordBoardColumnHeader = () => {
</StyledRightContainer>
</StyledHeaderContainer>
</StyledHeader>
{isBoardColumnMenuOpen && columnDefinition.actions.length > 0 && (
{isBoardColumnMenuOpen && (
<RecordBoardColumnDropdownMenu
onClose={handleBoardColumnMenuClose}
stageId={columnDefinition.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -36,8 +28,6 @@ export const RecordBoardColumnHeaderWrapper = ({
value={{
columnId,
columnDefinition: columnDefinition,
isFirstColumn: isFirstColumn,
isLastColumn: isLastColumn,
recordCount: recordIds.length,
recordIds,
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down
Loading

0 comments on commit e8d96cf

Please sign in to comment.