Allow moving columns left/right in Kanban view (#11827)
### Demo https://github.com/user-attachments/assets/9bc81c33-454d-4581-b06e-dbc0ea79a8dd Closes #11791 --------- Co-authored-by: prastoin <paul@twenty.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,21 +1,31 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
|
||||
import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useReorderRecordGroups';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
|
||||
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useHasSettingsPermission } from '@/settings/roles/hooks/useHasSettingsPermission';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconEyeOff,
|
||||
IconSettings,
|
||||
} from 'twenty-ui/display';
|
||||
import { SettingPermissionType } from '~/generated/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { IconEyeOff, IconSettings } from 'twenty-ui/display';
|
||||
|
||||
type UseRecordGroupActionsParams = {
|
||||
viewType: ViewType;
|
||||
@ -50,6 +60,16 @@ export const useRecordGroupActions = ({
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
const visibleRecordGroupIds = useRecoilComponentFamilyValueV2(
|
||||
visibleRecordGroupIdsComponentFamilySelector,
|
||||
viewType,
|
||||
);
|
||||
|
||||
const { reorderRecordGroups } = useReorderRecordGroups({
|
||||
viewBarId: objectMetadataItem.id,
|
||||
viewType,
|
||||
});
|
||||
|
||||
const navigateToSelectSettings = useCallback(() => {
|
||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||
|
||||
@ -73,33 +93,60 @@ export const useRecordGroupActions = ({
|
||||
const hasAccessToDataModelSettings = useHasSettingsPermission(
|
||||
SettingPermissionType.DATA_MODEL,
|
||||
);
|
||||
const currentIndex = visibleRecordGroupIds.findIndex(
|
||||
(id) => id === recordGroupDefinition.id,
|
||||
);
|
||||
const isCurrentRecordGroupNotFound = currentIndex === -1;
|
||||
|
||||
const recordGroupActions: RecordGroupAction[] = [];
|
||||
|
||||
if (hasAccessToDataModelSettings) {
|
||||
recordGroupActions.push({
|
||||
const recordGroupActions: RecordGroupAction[] = [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
label: t`Edit`,
|
||||
icon: IconSettings,
|
||||
position: 0,
|
||||
callback: () => {
|
||||
navigateToSelectSettings();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
recordGroupActions.push({
|
||||
id: 'hide',
|
||||
label: 'Hide',
|
||||
icon: IconEyeOff,
|
||||
position: 1,
|
||||
callback: () => {
|
||||
handleRecordGroupVisibilityChange({
|
||||
...recordGroupDefinition,
|
||||
isVisible: false,
|
||||
});
|
||||
condition: hasAccessToDataModelSettings,
|
||||
callback: navigateToSelectSettings,
|
||||
},
|
||||
});
|
||||
{
|
||||
id: 'moveRight',
|
||||
label: t`Move right`,
|
||||
icon: IconArrowRight,
|
||||
condition:
|
||||
!isCurrentRecordGroupNotFound &&
|
||||
currentIndex < visibleRecordGroupIds.length - 1,
|
||||
position: 1,
|
||||
callback: () =>
|
||||
reorderRecordGroups({
|
||||
fromIndex: currentIndex,
|
||||
toIndex: currentIndex + 1,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'moveLeft',
|
||||
label: t`Move left`,
|
||||
icon: IconArrowLeft,
|
||||
condition: !isCurrentRecordGroupNotFound && currentIndex > 0,
|
||||
position: 2,
|
||||
callback: () =>
|
||||
reorderRecordGroups({
|
||||
fromIndex: currentIndex,
|
||||
toIndex: currentIndex - 1,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'hide',
|
||||
label: t`Hide`,
|
||||
icon: IconEyeOff,
|
||||
position: 3,
|
||||
callback: () =>
|
||||
handleRecordGroupVisibilityChange({
|
||||
...recordGroupDefinition,
|
||||
isVisible: false,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return recordGroupActions;
|
||||
return recordGroupActions.filter(
|
||||
({ condition }) => isUndefined(condition) || condition !== false,
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder';
|
||||
import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useReorderRecordGroups';
|
||||
import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState';
|
||||
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
|
||||
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
|
||||
@ -30,15 +30,24 @@ export const useRecordGroupReorderConfirmationModal = ({
|
||||
isRecordGroupReorderConfirmationModalVisibleState,
|
||||
);
|
||||
|
||||
const [pendingDragEndReorder, setPendingDragEndReorder] =
|
||||
const [pendingDragEndHandlerParams, setPendingDragEndHandlerParams] =
|
||||
useState<Parameters<OnDragEndResponder> | null>(null);
|
||||
|
||||
const { handleOrderChange: handleRecordGroupOrderChange } =
|
||||
useRecordGroupReorder({
|
||||
viewBarId: recordIndexId,
|
||||
viewType,
|
||||
});
|
||||
const { reorderRecordGroups } = useReorderRecordGroups({
|
||||
viewBarId: recordIndexId,
|
||||
viewType,
|
||||
});
|
||||
|
||||
const handleDragEnd: OnDragEndResponder = (result) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
reorderRecordGroups({
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
};
|
||||
const isDragableSortRecordGroup = useRecoilComponentValueV2(
|
||||
recordIndexRecordGroupIsDraggableSortComponentSelector,
|
||||
);
|
||||
@ -47,32 +56,29 @@ export const useRecordGroupReorderConfirmationModal = ({
|
||||
recordIndexRecordGroupSortComponentState,
|
||||
);
|
||||
|
||||
const handleRecordGroupOrderChangeWithModal: OnDragEndResponder = (
|
||||
result,
|
||||
provided,
|
||||
) => {
|
||||
const handleDragEndWithModal: OnDragEndResponder = (result, provided) => {
|
||||
if (!isDragableSortRecordGroup) {
|
||||
setIsRecordGroupReorderConfirmationModalVisible(true);
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(null);
|
||||
setPendingDragEndReorder([result, provided]);
|
||||
setPendingDragEndHandlerParams([result, provided]);
|
||||
} else {
|
||||
handleRecordGroupOrderChange(result, provided);
|
||||
handleDragEnd(result, provided);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
if (!pendingDragEndReorder) {
|
||||
if (!pendingDragEndHandlerParams) {
|
||||
throw new Error('pendingDragEndReorder is not set');
|
||||
}
|
||||
|
||||
setRecordGroupSort(RecordGroupSort.Manual);
|
||||
setPendingDragEndReorder(null);
|
||||
handleRecordGroupOrderChange(...pendingDragEndReorder);
|
||||
setPendingDragEndHandlerParams(null);
|
||||
handleDragEnd(...pendingDragEndHandlerParams);
|
||||
goBackToPreviousDropdownFocusId();
|
||||
};
|
||||
|
||||
return {
|
||||
handleRecordGroupOrderChangeWithModal,
|
||||
handleRecordGroupOrderChangeWithModal: handleDragEndWithModal,
|
||||
handleRecordGroupReorderConfirmClick: handleConfirmClick,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
@ -11,19 +9,24 @@ import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordGroupHandlersParams = {
|
||||
type UseReorderRecordGroupsParams = {
|
||||
viewBarId: string;
|
||||
viewType: ViewType;
|
||||
};
|
||||
|
||||
export const useRecordGroupReorder = ({
|
||||
type ReorderRecordGroupsParams = {
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
};
|
||||
|
||||
export const useReorderRecordGroups = ({
|
||||
viewBarId,
|
||||
viewType,
|
||||
}: UseRecordGroupHandlersParams) => {
|
||||
}: UseReorderRecordGroupsParams) => {
|
||||
const { setRecordGroups } = useSetRecordGroups();
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
@ -33,13 +36,9 @@ export const useRecordGroupReorder = ({
|
||||
|
||||
const { saveViewGroups } = useSaveCurrentViewGroups();
|
||||
|
||||
const handleOrderChange: OnDragEndResponder = useRecoilCallback(
|
||||
const reorderRecordGroups = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result) => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
({ fromIndex, toIndex }: ReorderRecordGroupsParams) => {
|
||||
const visibleRecordGroupIds = getSnapshotValue(
|
||||
snapshot,
|
||||
visibleRecordGroupIdsFamilySelector(viewType),
|
||||
@ -48,8 +47,8 @@ export const useRecordGroupReorder = ({
|
||||
const reorderedVisibleRecordGroupIds = moveArrayItem(
|
||||
visibleRecordGroupIds,
|
||||
{
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
fromIndex,
|
||||
toIndex,
|
||||
},
|
||||
);
|
||||
|
||||
@ -96,6 +95,6 @@ export const useRecordGroupReorder = ({
|
||||
);
|
||||
|
||||
return {
|
||||
handleOrderChange,
|
||||
reorderRecordGroups,
|
||||
};
|
||||
};
|
||||
@ -1,8 +1,10 @@
|
||||
import { IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export type RecordGroupAction = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: IconComponent;
|
||||
position: number;
|
||||
callback: () => void;
|
||||
condition?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user