diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx
index af1db6e8f..b67fcc5f0 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx
@@ -1,23 +1,34 @@
import {
IconBaselineDensitySmall,
IconChevronLeft,
+ IconLayoutKanban,
+ IconLayoutList,
IconLayoutNavbar,
IconLayoutSidebarRight,
+ IconTable,
MenuItem,
+ MenuItemSelect,
MenuItemToggle,
} from 'twenty-ui';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
+import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForLayout';
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
+import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
+import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
+import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { ViewType } from '@/views/types/ViewType';
+import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
-import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
+import { isDefined } from 'twenty-shared/utils';
export const ObjectOptionsDropdownLayoutContent = () => {
const { t } = useLingui();
@@ -26,9 +37,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
const {
recordIndexId,
objectMetadataItem,
- viewType,
resetContent,
onContentChange,
+ dropdownId,
} = useOptionsDropdown();
const { isCompactModeActive, setAndPersistIsCompactModeActive } =
@@ -40,6 +51,31 @@ export const ObjectOptionsDropdownLayoutContent = () => {
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
+ const recordGroupFieldMetadata = useRecoilComponentValueV2(
+ recordGroupFieldMetadataComponentState,
+ );
+
+ const { setAndPersistViewType } = useSetViewTypeFromLayoutOptionsMenu();
+ const { availableFieldsForKanban, navigateToSelectSettings } =
+ useGetAvailableFieldsForKanban();
+
+ const { closeDropdown } = useDropdown(dropdownId);
+
+ const handleSelectKanbanViewType = async () => {
+ if (isDefaultView) {
+ return;
+ }
+ if (availableFieldsForKanban.length === 0) {
+ navigateToSelectSettings();
+ closeDropdown();
+ }
+ if (currentView?.type !== ViewType.Kanban) {
+ await setAndPersistViewType(ViewType.Kanban);
+ }
+ };
+
+ const isDefaultView = currentView?.key === 'INDEX';
+
return (
<>
{
>
{t`Layout`}
-
-
+ )}
>
);
};
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx
index 8462b4155..cf68237fd 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx
@@ -74,6 +74,8 @@ export const ObjectOptionsDropdownMenuContent = () => {
const theme = useTheme();
const { enqueueSnackBar } = useSnackBar();
+ const isDefaultView = currentView?.key === 'INDEX';
+
return (
<>
{currentView && (
@@ -110,14 +112,14 @@ export const ObjectOptionsDropdownMenuContent = () => {
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
- text={t`Group by`}
+ text={t`Group`}
contextualText={
- !isGroupByEnabled
+ isDefaultView
? t`Not available on Default View`
: recordGroupFieldMetadata?.label
}
hasSubMenu
- disabled={!isGroupByEnabled}
+ disabled={isDefaultView}
/>
{!isGroupByEnabled && (
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName.tsx
index 4ba918f4a..c8047eca7 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuViewName.tsx
@@ -37,6 +37,7 @@ const StyledMenuTitleContainer = styled.div`
`;
const StyledMenuIconContainer = styled.div`
+ color: ${({ theme }) => theme.font.color.primary};
align-items: center;
display: flex;
height: ${({ theme }) => theme.spacing(6)};
@@ -44,6 +45,7 @@ const StyledMenuIconContainer = styled.div`
width: ${({ theme }) => theme.spacing(6)};
`;
const StyledMainText = styled.div`
+ color: ${({ theme }) => theme.font.color.primary};
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx
index 07a37a4e5..8c42139db 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx
@@ -97,7 +97,7 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
/>
}
>
- Group by
+ Group
{currentView?.key !== 'INDEX' && (
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForLayout.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForLayout.ts
new file mode 100644
index 000000000..91e2cad04
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForLayout.ts
@@ -0,0 +1,158 @@
+import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
+import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
+
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
+import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
+import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
+import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
+import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
+import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
+import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
+import { GraphQLView } from '@/views/types/GraphQLView';
+import { ViewGroup } from '@/views/types/ViewGroup';
+import { ViewType } from '@/views/types/ViewType';
+import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
+import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState';
+import { useCallback } from 'react';
+import { useRecoilCallback, useSetRecoilState } from 'recoil';
+import { assertUnreachable, isDefined } from 'twenty-shared/utils';
+import { v4 } from 'uuid';
+
+export const useSetViewTypeFromLayoutOptionsMenu = () => {
+ const { updateCurrentView } = useUpdateCurrentView();
+ const setRecordIndexViewType = useSetRecoilState(recordIndexViewTypeState);
+ const { availableFieldsForKanban } = useGetAvailableFieldsForKanban();
+ const { objectMetadataItem } = useRecordIndexContextOrThrow();
+
+ const setViewPickerKanbanFieldMetadataId = useSetRecoilComponentStateV2(
+ viewPickerKanbanFieldMetadataIdComponentState,
+ );
+
+ const { loadRecordIndexStates } = useLoadRecordIndexStates();
+
+ const { createViewGroupRecords } = usePersistViewGroupRecords();
+
+ const createViewGroupAssociatedWithKanbanField = useCallback(
+ async (randomFieldForKanban: string, currentViewId: string) => {
+ const viewGroupsToCreate =
+ objectMetadataItem.fields
+ ?.find((field) => field.id === randomFieldForKanban)
+ ?.options?.map(
+ (option, index) =>
+ ({
+ id: v4(),
+ __typename: 'ViewGroup',
+ fieldMetadataId: randomFieldForKanban,
+ fieldValue: option.value,
+ isVisible: true,
+ position: index,
+ }) satisfies ViewGroup,
+ ) ?? [];
+
+ viewGroupsToCreate.push({
+ __typename: 'ViewGroup',
+ id: v4(),
+ fieldValue: '',
+ position: viewGroupsToCreate.length,
+ isVisible: true,
+ fieldMetadataId: randomFieldForKanban,
+ } satisfies ViewGroup);
+
+ await createViewGroupRecords({
+ viewGroupsToCreate,
+ viewId: currentViewId,
+ });
+
+ return viewGroupsToCreate;
+ },
+ [objectMetadataItem, createViewGroupRecords],
+ );
+
+ const setAndPersistViewType = useRecoilCallback(
+ ({ snapshot }) =>
+ async (viewType: ViewType) => {
+ const currentViewId = snapshot
+ .getLoadable(
+ contextStoreCurrentViewIdComponentState.atomFamily({
+ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
+ }),
+ )
+ .getValue();
+
+ if (!isDefined(currentViewId)) {
+ throw new Error('No view id found');
+ }
+ const currentView = snapshot
+ .getLoadable(
+ prefetchViewFromViewIdFamilySelector({ viewId: currentViewId }),
+ )
+ .getValue();
+ if (!isDefined(currentView)) {
+ throw new Error('No current view found');
+ }
+
+ const updateCurrentViewParams: Partial = {};
+ updateCurrentViewParams.type = viewType;
+
+ switch (viewType) {
+ case ViewType.Kanban: {
+ if (availableFieldsForKanban.length === 0) {
+ throw new Error('No fields for kanban - should not happen');
+ }
+ const previouslySelectedKanbanField = availableFieldsForKanban.find(
+ (fieldsForKanban) =>
+ fieldsForKanban.id === currentView.kanbanFieldMetadataId,
+ );
+
+ const kanbanField = isDefined(previouslySelectedKanbanField)
+ ? previouslySelectedKanbanField
+ : availableFieldsForKanban[0];
+
+ if (!isDefined(previouslySelectedKanbanField)) {
+ updateCurrentViewParams.kanbanFieldMetadataId = kanbanField.id;
+ }
+
+ const hasViewGroups = currentView.viewGroups.some(
+ (viewGroup: ViewGroup) =>
+ viewGroup.fieldMetadataId === kanbanField.id,
+ );
+
+ if (!hasViewGroups) {
+ const viewGroups = await createViewGroupAssociatedWithKanbanField(
+ kanbanField.id,
+ currentView.id,
+ );
+ loadRecordIndexStates(
+ { ...currentView, viewGroups },
+ objectMetadataItem,
+ );
+ setViewPickerKanbanFieldMetadataId(kanbanField.id);
+ }
+ setRecordIndexViewType(viewType);
+ await updateCurrentView(updateCurrentViewParams);
+ break;
+ }
+ case ViewType.Table:
+ setRecordIndexViewType(viewType);
+ await updateCurrentView(updateCurrentViewParams);
+ break;
+ default: {
+ return assertUnreachable(viewType);
+ }
+ }
+ },
+ [
+ availableFieldsForKanban,
+ objectMetadataItem,
+ updateCurrentView,
+ setRecordIndexViewType,
+ createViewGroupAssociatedWithKanbanField,
+ setViewPickerKanbanFieldMetadataId,
+ loadRecordIndexStates,
+ ],
+ );
+
+ return {
+ setAndPersistViewType,
+ };
+};