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:
@ -0,0 +1,192 @@
|
||||
import {
|
||||
DropResult,
|
||||
OnDragEndResponder,
|
||||
ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useRef } from 'react';
|
||||
import { IconEye, IconEyeOff, Tag } from 'twenty-ui';
|
||||
|
||||
import {
|
||||
RecordGroupDefinition,
|
||||
RecordGroupDefinitionType,
|
||||
} from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type ViewGroupsVisibilityDropdownSectionProps = {
|
||||
viewGroups: RecordGroupDefinition[];
|
||||
isDraggable: boolean;
|
||||
onDragEnd?: OnDragEndResponder;
|
||||
onVisibilityChange: (viewGroup: RecordGroupDefinition) => void;
|
||||
title: string;
|
||||
showSubheader: boolean;
|
||||
showDragGrip: boolean;
|
||||
};
|
||||
|
||||
export const ViewGroupsVisibilityDropdownSection = ({
|
||||
viewGroups,
|
||||
isDraggable,
|
||||
onDragEnd,
|
||||
onVisibilityChange,
|
||||
title,
|
||||
showSubheader = true,
|
||||
showDragGrip,
|
||||
}: ViewGroupsVisibilityDropdownSectionProps) => {
|
||||
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
|
||||
onDragEnd?.(result, provided);
|
||||
};
|
||||
|
||||
const getIconButtons = (index: number, viewGroup: RecordGroupDefinition) => {
|
||||
const iconButtons = [
|
||||
{
|
||||
Icon: viewGroup.isVisible ? IconEyeOff : IconEye,
|
||||
onClick: () => onVisibilityChange(viewGroup),
|
||||
},
|
||||
].filter(isDefined);
|
||||
|
||||
return iconButtons.length ? iconButtons : undefined;
|
||||
};
|
||||
|
||||
const noValueViewGroups =
|
||||
viewGroups.filter(
|
||||
(viewGroup) => viewGroup.type === RecordGroupDefinitionType.NoValue,
|
||||
) ?? [];
|
||||
|
||||
const viewGroupsWithoutNoValueGroups = viewGroups.filter(
|
||||
(viewGroup) => viewGroup.type !== RecordGroupDefinitionType.NoValue,
|
||||
);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{showSubheader && (
|
||||
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||
)}
|
||||
<DropdownMenuItemsContainer>
|
||||
{!!viewGroups.length && (
|
||||
<>
|
||||
{!isDraggable ? (
|
||||
viewGroupsWithoutNoValueGroups.map(
|
||||
(viewGroup, viewGroupIndex) => (
|
||||
<MenuItemDraggable
|
||||
key={viewGroup.id}
|
||||
text={
|
||||
<Tag
|
||||
variant={
|
||||
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||
? 'solid'
|
||||
: 'outline'
|
||||
}
|
||||
color={
|
||||
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||
? viewGroup.color
|
||||
: 'transparent'
|
||||
}
|
||||
text={viewGroup.title}
|
||||
weight={
|
||||
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||
? 'regular'
|
||||
: 'medium'
|
||||
}
|
||||
/>
|
||||
}
|
||||
iconButtons={getIconButtons(viewGroupIndex, viewGroup)}
|
||||
accent={showDragGrip ? 'placeholder' : 'default'}
|
||||
showGrip={showDragGrip}
|
||||
isDragDisabled={!isDraggable}
|
||||
/>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<DraggableList
|
||||
onDragEnd={handleOnDrag}
|
||||
draggableItems={
|
||||
<>
|
||||
{viewGroupsWithoutNoValueGroups.map(
|
||||
(viewGroup, viewGroupIndex) => (
|
||||
<DraggableItem
|
||||
key={viewGroup.id}
|
||||
draggableId={viewGroup.id}
|
||||
index={viewGroupIndex + 1}
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
key={viewGroup.id}
|
||||
text={
|
||||
<Tag
|
||||
variant={
|
||||
viewGroup.type !==
|
||||
RecordGroupDefinitionType.NoValue
|
||||
? 'solid'
|
||||
: 'outline'
|
||||
}
|
||||
color={
|
||||
viewGroup.type !==
|
||||
RecordGroupDefinitionType.NoValue
|
||||
? viewGroup.color
|
||||
: 'transparent'
|
||||
}
|
||||
text={viewGroup.title}
|
||||
weight={
|
||||
viewGroup.type !==
|
||||
RecordGroupDefinitionType.NoValue
|
||||
? 'regular'
|
||||
: 'medium'
|
||||
}
|
||||
/>
|
||||
}
|
||||
iconButtons={getIconButtons(
|
||||
viewGroupIndex,
|
||||
viewGroup,
|
||||
)}
|
||||
accent={showDragGrip ? 'placeholder' : 'default'}
|
||||
showGrip={showDragGrip}
|
||||
isDragDisabled={!isDraggable}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{noValueViewGroups.map((viewGroup) => (
|
||||
<MenuItemDraggable
|
||||
key={viewGroup.id}
|
||||
text={
|
||||
<Tag
|
||||
variant={
|
||||
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||
? 'solid'
|
||||
: 'outline'
|
||||
}
|
||||
color={
|
||||
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||
? viewGroup.color
|
||||
: 'transparent'
|
||||
}
|
||||
text={viewGroup.title}
|
||||
weight={
|
||||
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||
? 'regular'
|
||||
: 'medium'
|
||||
}
|
||||
/>
|
||||
}
|
||||
accent={showDragGrip ? 'placeholder' : 'default'}
|
||||
showGrip={true}
|
||||
isDragDisabled={true}
|
||||
isHoverDisabled
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,118 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useCallback } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
|
||||
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
|
||||
export const usePersistViewGroupRecords = () => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.ViewGroup,
|
||||
});
|
||||
|
||||
const { createOneRecordMutation } = useCreateOneRecordMutation({
|
||||
objectNameSingular: CoreObjectNameSingular.ViewGroup,
|
||||
});
|
||||
|
||||
const { updateOneRecordMutation } = useUpdateOneRecordMutation({
|
||||
objectNameSingular: CoreObjectNameSingular.ViewGroup,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const createViewGroupRecords = useCallback(
|
||||
(viewGroupsToCreate: ViewGroup[], view: GraphQLView) => {
|
||||
if (!viewGroupsToCreate.length) return;
|
||||
|
||||
return Promise.all(
|
||||
viewGroupsToCreate.map((viewGroup) =>
|
||||
apolloClient.mutate({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: {
|
||||
fieldMetadataId: viewGroup.fieldMetadataId,
|
||||
viewId: view.id,
|
||||
isVisible: viewGroup.isVisible,
|
||||
position: viewGroup.position,
|
||||
id: v4(),
|
||||
fieldValue: viewGroup.fieldValue,
|
||||
},
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.['createViewGroup'];
|
||||
if (!record) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToCreate: [record],
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[
|
||||
apolloClient,
|
||||
createOneRecordMutation,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
const updateViewGroupRecords = useCallback(
|
||||
async (viewGroupsToUpdate: ViewGroup[]) => {
|
||||
if (!viewGroupsToUpdate.length) return;
|
||||
|
||||
const mutationPromises = viewGroupsToUpdate.map((viewGroup) =>
|
||||
apolloClient.mutate<{ updateViewGroup: ViewGroup }>({
|
||||
mutation: updateOneRecordMutation,
|
||||
variables: {
|
||||
idToUpdate: viewGroup.id,
|
||||
input: {
|
||||
isVisible: viewGroup.isVisible,
|
||||
position: viewGroup.position,
|
||||
},
|
||||
},
|
||||
// Avoid cache being updated with stale data
|
||||
fetchPolicy: 'no-cache',
|
||||
}),
|
||||
);
|
||||
|
||||
const mutationResults = await Promise.all(mutationPromises);
|
||||
|
||||
// FixMe: Using triggerCreateRecordsOptimisticEffect is actaully causing multiple records to be created
|
||||
mutationResults.forEach(({ data }) => {
|
||||
const record = data?.['updateViewGroup'];
|
||||
|
||||
if (!record) return;
|
||||
|
||||
apolloClient.cache.modify({
|
||||
id: apolloClient.cache.identify({
|
||||
__typename: 'ViewGroup',
|
||||
id: record.id,
|
||||
}),
|
||||
fields: {
|
||||
isVisible: () => record.isVisible,
|
||||
position: () => record.position,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[apolloClient, updateOneRecordMutation],
|
||||
);
|
||||
|
||||
return {
|
||||
createViewGroupRecords,
|
||||
updateViewGroupRecords,
|
||||
};
|
||||
};
|
||||
@ -1,34 +0,0 @@
|
||||
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
|
||||
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
||||
|
||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useCreateViewFiltersAndSorts = () => {
|
||||
const { getViewFromCache } = useGetViewFromCache();
|
||||
|
||||
const { createViewSortRecords } = usePersistViewSortRecords();
|
||||
|
||||
const { createViewFilterRecords } = usePersistViewFilterRecords();
|
||||
|
||||
const createViewFiltersAndSorts = async (
|
||||
viewIdToCreateOn: string,
|
||||
filtersToCreate: ViewFilter[],
|
||||
sortsToCreate: ViewSort[],
|
||||
) => {
|
||||
const view = await getViewFromCache(viewIdToCreateOn);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createViewSortRecords(sortsToCreate, view);
|
||||
await createViewFilterRecords(filtersToCreate, view);
|
||||
};
|
||||
|
||||
return {
|
||||
createViewFiltersAndSorts,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
|
||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const useSaveCurrentViewGroups = (viewBarComponentId?: string) => {
|
||||
const { createViewGroupRecords, updateViewGroupRecords } =
|
||||
usePersistViewGroupRecords();
|
||||
|
||||
const { getViewFromCache } = useGetViewFromCache();
|
||||
|
||||
const currentViewIdCallbackState = useRecoilComponentCallbackStateV2(
|
||||
currentViewIdComponentState,
|
||||
viewBarComponentId,
|
||||
);
|
||||
|
||||
const saveViewGroups = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (viewGroupsToSave: ViewGroup[]) => {
|
||||
const currentViewId = snapshot
|
||||
.getLoadable(currentViewIdCallbackState)
|
||||
.getValue();
|
||||
|
||||
if (!currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = await getViewFromCache(currentViewId);
|
||||
|
||||
if (isUndefinedOrNull(view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentViewGroups = view.viewGroups;
|
||||
|
||||
const viewGroupsToUpdate = viewGroupsToSave
|
||||
.map((viewGroupToSave) => {
|
||||
const existingField = currentViewGroups.find(
|
||||
(currentViewGroup) =>
|
||||
currentViewGroup.fieldValue === viewGroupToSave.fieldValue,
|
||||
);
|
||||
|
||||
if (isUndefinedOrNull(existingField)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
isDeeplyEqual(
|
||||
{
|
||||
position: existingField.position,
|
||||
isVisible: existingField.isVisible,
|
||||
},
|
||||
{
|
||||
position: viewGroupToSave.position,
|
||||
isVisible: viewGroupToSave.isVisible,
|
||||
},
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { ...viewGroupToSave, id: existingField.id };
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const viewGroupsToCreate = viewGroupsToSave.filter(
|
||||
(viewFieldToSave) =>
|
||||
!currentViewGroups.some(
|
||||
(currentViewGroup) =>
|
||||
currentViewGroup.fieldValue === viewFieldToSave.fieldValue,
|
||||
),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
createViewGroupRecords(viewGroupsToCreate, view),
|
||||
updateViewGroupRecords(viewGroupsToUpdate),
|
||||
]);
|
||||
},
|
||||
[
|
||||
createViewGroupRecords,
|
||||
currentViewIdCallbackState,
|
||||
getViewFromCache,
|
||||
updateViewGroupRecords,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
saveViewGroups,
|
||||
};
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
@ -15,6 +16,7 @@ export type GraphQLView = {
|
||||
viewFields: ViewField[];
|
||||
viewFilters: ViewFilter[];
|
||||
viewSorts: ViewSort[];
|
||||
viewGroups: ViewGroup[];
|
||||
position: number;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
@ -12,6 +13,7 @@ export type View = {
|
||||
objectMetadataId: string;
|
||||
isCompact: boolean;
|
||||
viewFields: ViewField[];
|
||||
viewGroups: ViewGroup[];
|
||||
viewFilters: ViewFilter[];
|
||||
viewSorts: ViewSort[];
|
||||
kanbanFieldMetadataId: string;
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
export type ViewGroup = {
|
||||
__typename: 'ViewGroup';
|
||||
id: string;
|
||||
fieldMetadataId: string;
|
||||
isVisible: boolean;
|
||||
fieldValue: string;
|
||||
position: number;
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
|
||||
export const mapRecordGroupDefinitionsToViewGroups = (
|
||||
groupDefinitions: RecordGroupDefinition[],
|
||||
): ViewGroup[] => {
|
||||
return groupDefinitions.map(
|
||||
(groupDefinition): ViewGroup => ({
|
||||
__typename: 'ViewGroup',
|
||||
id: groupDefinition.id,
|
||||
fieldMetadataId: groupDefinition.fieldMetadataId,
|
||||
position: groupDefinition.position,
|
||||
isVisible: groupDefinition.isVisible ?? true,
|
||||
fieldValue: groupDefinition.value ?? '',
|
||||
}),
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,79 @@
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
RecordGroupDefinition,
|
||||
RecordGroupDefinitionType,
|
||||
} from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const mapViewGroupsToRecordGroupDefinitions = ({
|
||||
objectMetadataItem,
|
||||
viewGroups,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
viewGroups: ViewGroup[];
|
||||
}): RecordGroupDefinition[] => {
|
||||
if (viewGroups?.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fieldMetadataId = viewGroups?.[0]?.fieldMetadataId;
|
||||
const selectFieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.id === fieldMetadataId && field.type === FieldMetadataType.Select,
|
||||
);
|
||||
|
||||
if (!selectFieldMetadataItem) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!selectFieldMetadataItem.options) {
|
||||
throw new Error(
|
||||
`Select Field ${objectMetadataItem.nameSingular} has no options`,
|
||||
);
|
||||
}
|
||||
|
||||
const recordGroupDefinitionsFromViewGroups = viewGroups
|
||||
.map((viewGroup) => {
|
||||
const selectedOption = selectFieldMetadataItem.options?.find(
|
||||
(option) => option.value === viewGroup.fieldValue,
|
||||
);
|
||||
|
||||
if (!selectedOption) return null;
|
||||
|
||||
return {
|
||||
id: viewGroup.id,
|
||||
fieldMetadataId: viewGroup.fieldMetadataId,
|
||||
type: RecordGroupDefinitionType.Value,
|
||||
title: selectedOption.label,
|
||||
value: selectedOption.value,
|
||||
color: selectedOption.color,
|
||||
position: viewGroup.position,
|
||||
isVisible: viewGroup.isVisible,
|
||||
} as RecordGroupDefinition;
|
||||
})
|
||||
.filter(isDefined)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
if (selectFieldMetadataItem.isNullable === true) {
|
||||
const noValueColumn = {
|
||||
id: 'no-value',
|
||||
title: 'No Value',
|
||||
type: RecordGroupDefinitionType.NoValue,
|
||||
value: null,
|
||||
position:
|
||||
recordGroupDefinitionsFromViewGroups
|
||||
.map((option) => option.position)
|
||||
.reduce((a, b) => Math.max(a, b), 0) + 1,
|
||||
isVisible: true,
|
||||
fieldMetadataId: selectFieldMetadataItem.id,
|
||||
color: 'transparent',
|
||||
} satisfies RecordGroupDefinition;
|
||||
|
||||
return [...recordGroupDefinitionsFromViewGroups, noValueColumn];
|
||||
}
|
||||
|
||||
return recordGroupDefinitionsFromViewGroups;
|
||||
};
|
||||
Reference in New Issue
Block a user