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