Skip to content

Commit 0fb60b3

Browse files
committed
Prevent folders from being dropped into their descendant
1 parent ad1080a commit 0fb60b3

File tree

4 files changed

+59
-22
lines changed

4 files changed

+59
-22
lines changed

packages/chonky/src/components/external/FolderChainButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const FolderChainButton: React.FC<FolderChainButtonProps> = React.memo(
2626
const { file, disabled, onClick } = item;
2727
const { dndIsOver, dndCanDrop, drop } = useFileDrop({
2828
file,
29-
forceDisableDrop: !!file && !current,
29+
forceDisableDrop: !file || current,
3030
});
3131
const dndState = useMemo<DndEntryState>(
3232
() => ({

packages/chonky/src/components/file-list/FileList.tsx

+37-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useContext } from 'react';
1+
import React, { useCallback, useContext, useMemo } from 'react';
22
import { useSelector } from 'react-redux';
33
import AutoSizer from 'react-virtualized-auto-sizer';
44

@@ -24,14 +24,23 @@ import { ListContainer } from './ListContainer';
2424

2525
export interface FileListProps {}
2626

27+
interface StyleState {
28+
dndCanDrop: boolean;
29+
dndIsOverCurrent: boolean;
30+
}
31+
2732
export const FileList: React.FC<FileListProps> = React.memo(() => {
2833
const displayFileIds = useSelector(selectDisplayFileIds);
2934
const viewConfig = useSelector(selectFileViewConfig);
3035

3136
const currentFolder = useSelector(selectCurrentFolder);
32-
const { dndCanDrop, drop } = useFileDrop({ file: currentFolder });
37+
const { drop, dndCanDrop, dndIsOverCurrent } = useFileDrop({ file: currentFolder });
38+
const styleState = useMemo<StyleState>(() => ({ dndCanDrop, dndIsOverCurrent }), [
39+
dndCanDrop,
40+
dndIsOverCurrent,
41+
]);
3342

34-
const localClasses = useLocalStyles(dndCanDrop);
43+
const localClasses = useLocalStyles(styleState);
3544
const classes = useStyles(viewConfig);
3645

3746
// In Chonky v0.x, this field was user-configurable. In Chonky v1.x+, we hardcode
@@ -61,7 +70,13 @@ export const FileList: React.FC<FileListProps> = React.memo(() => {
6170
>
6271
<div className={localClasses.dndDropZone}>
6372
<div className={localClasses.dndDropZoneIcon}>
64-
<ChonkyIcon icon={ChonkyIconName.dndCanDrop} />
73+
<ChonkyIcon
74+
icon={
75+
dndCanDrop
76+
? ChonkyIconName.dndCanDrop
77+
: ChonkyIconName.dndCannotDrop
78+
}
79+
/>
6580
</div>
6681
</div>
6782
<AutoSizer disableHeight={!fillParentContainer}>{listRenderer}</AutoSizer>
@@ -72,16 +87,23 @@ export const FileList: React.FC<FileListProps> = React.memo(() => {
7287
const useLocalStyles = makeLocalChonkyStyles((theme) => ({
7388
fileListWrapper: {
7489
minHeight: ChonkyActions.EnableGridView.fileViewConfig.entryHeight + 2,
75-
background: (showIndicator: boolean) =>
76-
showIndicator
77-
? getStripeGradient(
78-
theme.dnd.fileListMaskOne,
79-
theme.dnd.fileListMaskTwo
80-
)
90+
background: (state: StyleState) =>
91+
state.dndIsOverCurrent && state.dndCanDrop
92+
? state.dndCanDrop
93+
? getStripeGradient(
94+
theme.dnd.fileListCanDropMaskOne,
95+
theme.dnd.fileListCanDropMaskTwo
96+
)
97+
: getStripeGradient(
98+
theme.dnd.fileListCannotDropMaskOne,
99+
theme.dnd.fileListCannotDropMaskTwo
100+
)
81101
: 'none',
82102
},
83103
dndDropZone: {
84-
display: (showIndicator: boolean) => (showIndicator ? 'block' : 'none'),
104+
display: (state: StyleState) =>
105+
// When we cannot drop, we don't show an indicator at all
106+
state.dndIsOverCurrent && state.dndCanDrop ? 'block' : 'none',
85107
borderRadius: theme.gridFileEntry.borderRadius,
86108
pointerEvents: 'none',
87109
position: 'absolute',
@@ -90,10 +112,12 @@ const useLocalStyles = makeLocalChonkyStyles((theme) => ({
90112
zIndex: 2,
91113
},
92114
dndDropZoneIcon: {
115+
backgroundColor: (state: StyleState) =>
116+
state.dndCanDrop ? theme.dnd.canDropMask : theme.dnd.cannotDropMask,
117+
color: (state: StyleState) =>
118+
state.dndCanDrop ? theme.dnd.canDropColor : theme.dnd.cannotDropColor,
93119
borderRadius: theme.gridFileEntry.borderRadius,
94120
transform: 'translateX(-50%) translateY(-50%)',
95-
backgroundColor: theme.dnd.canDropMask,
96-
color: theme.dnd.canDropColor,
97121
position: 'absolute',
98122
textAlign: 'center',
99123
lineHeight: '60px',

packages/chonky/src/util/dnd.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { EssentialActions } from '../action-definitions/essential';
88
import { ChonkyActions } from '../action-definitions/index';
99
import {
1010
selectCurrentFolder,
11+
selectFolderChain,
1112
selectInstanceId,
1213
selectSelectedFiles,
1314
} from '../redux/selectors';
@@ -115,6 +116,7 @@ export const useFileDrop = ({
115116
forceDisableDrop,
116117
includeChildrenDrops,
117118
}: UseFileDropParams) => {
119+
const folderChainRef = useInstanceVariable(useSelector(selectFolderChain));
118120
const onDrop = useCallback(
119121
(item: ChonkyDndFileEntryItem, monitor) => {
120122
if (!monitor.canDrop()) return;
@@ -136,15 +138,24 @@ export const useFileDrop = ({
136138
}
137139
const { source, draggedFile, selectedFiles } = item.payload;
138140

139-
const filesToCheck: FileData[] = [draggedFile, ...selectedFiles];
140-
if (source) filesToCheck.push(source);
141-
for (const currFile of filesToCheck) {
142-
if (file.id === currFile.id) return false;
141+
// We prevent folders from being dropped into themselves. We also prevent
142+
// any folder from current folder chain being moved - we can't move the
143+
// folder that we are currently in.
144+
const prohibitedFileIds = new Set<string>();
145+
prohibitedFileIds.add(file.id);
146+
folderChainRef.current.map((folder) => {
147+
if (folder) prohibitedFileIds.add(folder.id);
148+
});
149+
const movedFiles: FileData[] = [draggedFile, ...selectedFiles];
150+
for (const currFile of movedFiles) {
151+
if (prohibitedFileIds.has(currFile.id)) return false;
143152
}
144153

145-
return true;
154+
// Finally, prohibit files from being moved into their parent folder
155+
// (which is a no-op).
156+
return file.id !== source?.id;
146157
},
147-
[forceDisableDrop, file, includeChildrenDrops]
158+
[forceDisableDrop, file, includeChildrenDrops, folderChainRef]
148159
);
149160
const collect = useCallback(
150161
(monitor) => ({

packages/chonky/src/util/styles.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ export const lightTheme = {
3939
cannotDropColor: 'red',
4040
canDropMask: 'rgba(180, 235, 180, 0.75)',
4141
cannotDropMask: 'rgba(235, 180, 180, 0.75)',
42-
fileListMaskOne: 'rgba(180, 235, 180, 0.1)',
43-
fileListMaskTwo: 'rgba(180, 235, 180, 0.2)',
42+
fileListCanDropMaskOne: 'rgba(180, 235, 180, 0.1)',
43+
fileListCanDropMaskTwo: 'rgba(180, 235, 180, 0.2)',
44+
fileListCannotDropMaskOne: 'rgba(235, 180, 180, 0.1)',
45+
fileListCannotDropMaskTwo: 'rgba(235, 180, 180, 0.2)',
4446
},
4547

4648
dragLayer: {

0 commit comments

Comments
 (0)