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:
Jérémy M
2024-10-24 15:38:52 +02:00
committed by GitHub
parent 68a060a046
commit e8d96cfd10
61 changed files with 1408 additions and 508 deletions

View File

@ -1,9 +1,12 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
import { useCreateViewFiltersAndSorts } from '@/views/hooks/useCreateViewFiltersAndSorts';
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
import { useGetViewFiltersCombined } from '@/views/hooks/useGetCombinedViewFilters';
import { useGetViewSortsCombined } from '@/views/hooks/useGetCombinedViewSorts';
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
@ -11,6 +14,10 @@ import { currentViewIdComponentState } from '@/views/states/currentViewIdCompone
import { isPersistingViewFieldsComponentState } from '@/views/states/isPersistingViewFieldsComponentState';
import { GraphQLView } from '@/views/types/GraphQLView';
import { View } from '@/views/types/View';
import { ViewGroup } from '@/views/types/ViewGroup';
import { ViewType } from '@/views/types/ViewType';
import { isNonEmptyArray } from '@sniptt/guards';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui';
import { v4 } from 'uuid';
@ -35,12 +42,18 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const { createViewFieldRecords } = usePersistViewFieldRecords();
const { createViewFiltersAndSorts } = useCreateViewFiltersAndSorts();
const { getViewSortsCombined } = useGetViewSortsCombined(viewBarComponentId);
const { getViewFiltersCombined } =
useGetViewFiltersCombined(viewBarComponentId);
const { createViewSortRecords } = usePersistViewSortRecords();
const { createViewGroupRecords } = usePersistViewGroupRecords();
const { createViewFilterRecords } = usePersistViewFilterRecords();
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
const createViewFromCurrentView = useRecoilCallback(
({ snapshot, set }) =>
async (
@ -93,20 +106,56 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
await createViewFieldRecords(view.viewFields, newView);
if (type === ViewType.Kanban) {
if (!isNonEmptyArray(view.viewGroups)) {
if (!isDefined(kanbanFieldMetadataId)) {
throw new Error('Kanban view must have a kanban field');
}
const viewGroupsToCreate =
objectMetadataItem?.fields
?.find((field) => field.id === kanbanFieldMetadataId)
?.options?.map(
(option, index) =>
({
id: v4(),
__typename: 'ViewGroup',
fieldMetadataId: kanbanFieldMetadataId,
fieldValue: option.value,
isVisible: true,
position: index,
}) satisfies ViewGroup,
) ?? [];
viewGroupsToCreate.push({
__typename: 'ViewGroup',
id: v4(),
fieldValue: '',
position: viewGroupsToCreate.length,
isVisible: true,
fieldMetadataId: kanbanFieldMetadataId,
} satisfies ViewGroup);
await createViewGroupRecords(viewGroupsToCreate, newView);
} else {
await createViewGroupRecords(view.viewGroups, newView);
}
}
if (shouldCopyFiltersAndSorts === true) {
const sourceViewCombinedFilters = getViewFiltersCombined(view.id);
const sourceViewCombinedSorts = getViewSortsCombined(view.id);
await createViewFiltersAndSorts(
newView.id,
sourceViewCombinedFilters,
sourceViewCombinedSorts,
);
await createViewSortRecords(sourceViewCombinedSorts, view);
await createViewFilterRecords(sourceViewCombinedFilters, view);
}
set(isPersistingViewFieldsCallbackState, false);
},
[
objectMetadataItem,
createViewSortRecords,
createViewFilterRecords,
createOneRecord,
createViewFieldRecords,
getViewSortsCombined,
@ -114,7 +163,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
currentViewIdCallbackState,
getViewFromCache,
isPersistingViewFieldsCallbackState,
createViewFiltersAndSorts,
createViewGroupRecords,
],
);