Skip to content

Commit 23951d3

Browse files
arthurdennerCompuIves
authored andcommitted
feat(explorer): improve discard changes functionality (codesandbox#3040)
* style(button): add overflow behavior to shared Button component * feat(explorer): add option to discard changes on unsynced files * refactor(explorer): confirm before discarding changes and simplify modal management * fix(recover): adapt message to number of recovered files
1 parent 3c95a71 commit 23951d3

File tree

7 files changed

+145
-115
lines changed

7 files changed

+145
-115
lines changed

packages/app/src/app/overmind/namespaces/files/internalActions.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@ export const recoverFiles: Action = ({ effects, actions, state }) => {
4242
return false;
4343
})
4444
.filter(Boolean);
45+
const numRecoveredFiles = recoveredList.length;
4546

46-
if (recoveredList.length > 0) {
47+
if (numRecoveredFiles > 0) {
4748
effects.analytics.track('Files Recovered', {
48-
fileCount: recoveredList.length,
49+
fileCount: numRecoveredFiles,
4950
});
5051

5152
effects.notificationToast.add({
52-
message: `We recovered ${
53-
recoveredList.length
54-
} unsaved files from a previous session`,
53+
message: `We recovered ${numRecoveredFiles} unsaved ${
54+
numRecoveredFiles > 1 ? 'files' : 'file'
55+
} from a previous session`,
5556
status: NotificationStatus.NOTICE,
5657
});
5758
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import { Alert } from 'app/components/Alert';
3+
import Modal from 'app/components/Modal';
4+
5+
interface DirectoryEntryModalProps {
6+
body: React.ReactNode;
7+
isOpen: boolean;
8+
onClose: () => void;
9+
onConfirm: () => void;
10+
title: string;
11+
}
12+
13+
const DirectoryEntryModal = ({
14+
body,
15+
isOpen,
16+
onClose,
17+
onConfirm,
18+
title,
19+
}: DirectoryEntryModalProps) => (
20+
<Modal isOpen={isOpen} onClose={onClose} width={400}>
21+
<Alert
22+
css={`
23+
background-color: ${props =>
24+
props.theme['sideBar.background'] || 'auto'};
25+
color: ${props =>
26+
props.theme.light ? 'rgba(0,0,0,0.9)' : 'rgba(255,255,255,0.9)'};
27+
`}
28+
title={title}
29+
body={body}
30+
onCancel={onClose}
31+
onConfirm={onConfirm}
32+
/>
33+
</Modal>
34+
);
35+
36+
export default DirectoryEntryModal;

packages/app/src/app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/Entry/EditIcons/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import AddFileIcon from 'react-icons/lib/md/insert-drive-file';
66
import AddDirectoryIcon from 'react-icons/lib/md/create-new-folder';
77
import UploadFileIcon from 'react-icons/lib/md/file-upload';
88
import DownloadIcon from 'react-icons/lib/md/file-download';
9+
import UndoIcon from 'react-icons/lib/md/undo';
910

1011
import Tooltip from '@codesandbox/common/lib/components/Tooltip';
1112

@@ -22,6 +23,7 @@ function EditIcons({
2223
className = undefined,
2324
hovering,
2425
onDelete,
26+
onDiscardChanges,
2527
onEdit,
2628
onCreateFile,
2729
onCreateDirectory,
@@ -54,6 +56,13 @@ function EditIcons({
5456
</Icon>
5557
</Tooltip>
5658
)}
59+
{onDiscardChanges && (
60+
<Tooltip content="Discard Changes">
61+
<Icon onClick={handleClick(onDiscardChanges)}>
62+
<UndoIcon />
63+
</Icon>
64+
</Tooltip>
65+
)}
5766
{onEdit && (
5867
<Tooltip content="Rename">
5968
<Icon onClick={handleClick(onEdit)}>

packages/app/src/app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/Entry/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import DeleteIcon from 'react-icons/lib/go/trashcan';
77
import AddDirectoryIcon from 'react-icons/lib/md/create-new-folder';
88
import UploadFileIcon from 'react-icons/lib/md/file-upload';
99
import AddFileIcon from 'react-icons/lib/md/insert-drive-file';
10+
import UndoIcon from 'react-icons/lib/md/undo';
1011

1112
import { EntryContainer } from '../../../elements';
1213
import EditIcons from './EditIcons';
@@ -25,7 +26,7 @@ interface IEntryProps {
2526
depth: number;
2627
type: string;
2728
active: boolean;
28-
discardModuleChanges: (shortid: string) => void;
29+
discardModuleChanges: (shortid: string, title: string) => void;
2930
setCurrentModule: (id: string) => void;
3031
connectDragSource: (node: JSX.Element) => JSX.Element;
3132
onCreateDirectoryClick: () => boolean | void;
@@ -95,7 +96,7 @@ const Entry: React.FC<IEntryProps> = ({
9596
deleteEntry ? deleteEntry(shortid, title) : false;
9697

9798
const discardModuleChangesAction = () =>
98-
discardModuleChanges ? discardModuleChanges(shortid) : false;
99+
discardModuleChanges ? discardModuleChanges(shortid, title) : false;
99100

100101
const handleRename = (newTitle: string, force: boolean = false) => {
101102
if (newTitle === title) {
@@ -123,6 +124,7 @@ const Entry: React.FC<IEntryProps> = ({
123124
isNotSynced && {
124125
title: 'Discard Changes',
125126
action: discardModuleChangesAction,
127+
icon: UndoIcon,
126128
},
127129
].filter(Boolean),
128130
[
@@ -205,6 +207,7 @@ const Entry: React.FC<IEntryProps> = ({
205207
onCreateFile={onCreateModuleClick}
206208
onCreateDirectory={onCreateDirectoryClick}
207209
onUploadFile={onUploadFileClick}
210+
onDiscardChanges={isNotSynced && discardModuleChangesAction}
208211
onDelete={deleteEntry && deleteAction}
209212
onEdit={rename && renameAction}
210213
active={active}

packages/app/src/app/pages/Sandbox/Editor/Workspace/Files/DirectoryEntry/index.js

Lines changed: 77 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { inject, observer } from 'app/componentConnectors';
2-
import { Alert } from 'app/components/Alert';
3-
import Modal from 'app/components/Modal';
42
import { reaction } from 'mobx';
53
import React from 'react';
64
import { DropTarget } from 'react-dnd';
75
import { NativeTypes } from 'react-dnd-html5-backend';
86
import { getChildren } from '@codesandbox/common/lib/sandbox/modules';
97

108
import DirectoryChildren from './DirectoryChildren';
9+
import DirectoryEntryModal from './DirectoryEntryModal';
1110
import { EntryContainer, Opener, Overlay } from './elements';
1211
import Entry from './Entry';
1312
import validateTitle from './validateTitle';
@@ -46,10 +45,8 @@ class DirectoryEntry extends React.PureComponent {
4645
this.state = {
4746
creating: '',
4847
open: props.root || store.editor.shouldDirectoryBeOpen(id),
49-
showDeleteDirectoryModal: false,
50-
showDeleteModuleModal: false,
51-
moduleToDeleteTitle: null,
52-
moduleToDeleteShortid: null,
48+
modalConfig: {},
49+
isModalOpen: false,
5350
};
5451
}
5552

@@ -104,11 +101,33 @@ class DirectoryEntry extends React.PureComponent {
104101
this.props.signals.files.moduleRenamed({ moduleShortid, title });
105102
};
106103

107-
deleteModule = (shortid, title) => {
104+
confirmDeleteModule = (shortid, moduleName) => {
108105
this.setState({
109-
showDeleteModuleModal: true,
110-
moduleToDeleteShortid: shortid,
111-
moduleToDeleteTitle: title,
106+
isModalOpen: true,
107+
modalConfig: {
108+
title: 'Delete File',
109+
body: (
110+
<span>
111+
Are you sure you want to delete{' '}
112+
<b
113+
css={`
114+
word-break: break-all;
115+
`}
116+
>
117+
{moduleName}
118+
</b>
119+
?
120+
<br />
121+
The file will be permanently removed.
122+
</span>
123+
),
124+
onConfirm: () => {
125+
this.closeModal();
126+
this.props.signals.files.moduleDeleted({
127+
moduleShortid: shortid,
128+
});
129+
},
130+
},
112131
});
113132
};
114133

@@ -149,16 +168,31 @@ class DirectoryEntry extends React.PureComponent {
149168
this.props.signals.files.directoryRenamed({ title, directoryShortid });
150169
};
151170

152-
closeModals = () => {
171+
closeModal = () => {
153172
this.setState({
154-
showDeleteDirectoryModal: false,
155-
showDeleteModuleModal: false,
173+
isModalOpen: false,
156174
});
157175
};
158176

159-
deleteDirectory = () => {
177+
confirmDeleteDirectory = (shortid, directoryName) => {
160178
this.setState({
161-
showDeleteDirectoryModal: true,
179+
isModalOpen: true,
180+
modalConfig: {
181+
title: 'Delete Directory',
182+
body: (
183+
<span>
184+
Are you sure you want to delete <b>{directoryName}</b>?
185+
<br />
186+
The directory will be permanently removed.
187+
</span>
188+
),
189+
onConfirm: () => {
190+
this.closeModal();
191+
this.props.signals.files.directoryDeleted({
192+
directoryShortid: shortid,
193+
});
194+
},
195+
},
162196
});
163197
};
164198

@@ -199,10 +233,24 @@ class DirectoryEntry extends React.PureComponent {
199233
this.props.signals.editor.moduleDoubleClicked();
200234
};
201235

202-
discardChanges = moduleShortid => {
203-
this.props.signals.editor.discardModuleChanges({ moduleShortid });
204-
205-
return true;
236+
confirmDiscardChanges = (shortid, moduleName) => {
237+
this.setState({
238+
isModalOpen: true,
239+
modalConfig: {
240+
title: 'Discard Changes',
241+
body: (
242+
<span>
243+
Are you sure you want to discard changes on <b>{moduleName}</b>?
244+
</span>
245+
),
246+
onConfirm: () => {
247+
this.closeModal();
248+
this.props.signals.editor.discardModuleChanges({
249+
moduleShortid: shortid,
250+
});
251+
},
252+
},
253+
});
206254
};
207255

208256
render() {
@@ -216,7 +264,7 @@ class DirectoryEntry extends React.PureComponent {
216264
store,
217265
getModulePath,
218266
} = this.props;
219-
const { creating, open } = this.state;
267+
const { creating, isModalOpen, modalConfig, open } = this.state;
220268
const { currentSandbox } = store.editor;
221269

222270
const title = root
@@ -238,7 +286,7 @@ class DirectoryEntry extends React.PureComponent {
238286
isOpen={open}
239287
onClick={this.toggleOpen}
240288
renameValidator={this.validateDirectoryTitle}
241-
discardModuleChanges={this.discardChanges}
289+
discardModuleChanges={this.confirmDiscardChanges}
242290
rename={!root && this.renameDirectory}
243291
onCreateModuleClick={this.onCreateModuleClick}
244292
onCreateDirectoryClick={this.onCreateDirectoryClick}
@@ -247,38 +295,11 @@ class DirectoryEntry extends React.PureComponent {
247295
currentSandbox.privacy === 0 &&
248296
this.onUploadFileClick
249297
}
250-
deleteEntry={!root && this.deleteDirectory}
298+
deleteEntry={!root && this.confirmDeleteDirectory}
251299
hasChildren={this.getChildren().length > 0}
252300
closeTree={this.closeTree}
253301
getModulePath={getModulePath}
254302
/>
255-
{this.state.showDeleteDirectoryModal && (
256-
<Modal
257-
isOpen={this.state.showDeleteDirectoryModal}
258-
onClose={this.closeModals}
259-
width={400}
260-
>
261-
<Alert
262-
title="Delete Directory"
263-
body={
264-
<span>
265-
Are you sure you want to delete <b>{title}</b>?
266-
<br />
267-
The directory will be permanently removed.
268-
</span>
269-
}
270-
onCancel={this.closeModals}
271-
onConfirm={() => {
272-
this.setState({
273-
showDeleteDirectoryModal: false,
274-
});
275-
this.props.signals.files.directoryDeleted({
276-
directoryShortid: shortid,
277-
});
278-
}}
279-
/>
280-
</Modal>
281-
)}
282303
</EntryContainer>
283304
)}
284305
<Opener open={open}>
@@ -299,55 +320,17 @@ class DirectoryEntry extends React.PureComponent {
299320
renameModule={this.renameModule}
300321
parentShortid={shortid}
301322
renameValidator={this.validateModuleTitle}
302-
deleteEntry={this.deleteModule}
323+
deleteEntry={this.confirmDeleteModule}
303324
setCurrentModule={this.setCurrentModule}
304325
markTabsNotDirty={this.markTabsNotDirty}
305-
discardModuleChanges={this.discardChanges}
326+
discardModuleChanges={this.confirmDiscardChanges}
306327
getModulePath={getModulePath}
307328
/>
308-
{this.state.showDeleteModuleModal && (
309-
<Modal
310-
isOpen={this.state.showDeleteModuleModal}
311-
onClose={this.closeModals}
312-
width={400}
313-
>
314-
<Alert
315-
css={`
316-
background-color: ${props =>
317-
props.theme['sideBar.background'] || 'auto'};
318-
color: ${props =>
319-
props.theme.light
320-
? 'rgba(0,0,0,0.9)'
321-
: 'rgba(255,255,255,0.9)'};
322-
`}
323-
title="Delete File"
324-
body={
325-
<span>
326-
Are you sure you want to delete{' '}
327-
<b
328-
css={`
329-
word-break: break-all;
330-
`}
331-
>
332-
{this.state.moduleToDeleteTitle}
333-
</b>
334-
?
335-
<br />
336-
The file will be permanently removed.
337-
</span>
338-
}
339-
onCancel={this.closeModals}
340-
onConfirm={() => {
341-
this.setState({
342-
showDeleteModuleModal: false,
343-
});
344-
this.props.signals.files.moduleDeleted({
345-
moduleShortid: this.state.moduleToDeleteShortid,
346-
});
347-
}}
348-
/>
349-
</Modal>
350-
)}
329+
<DirectoryEntryModal
330+
isOpen={isModalOpen}
331+
onClose={this.closeModal}
332+
{...modalConfig}
333+
/>
351334
{creating === 'module' && (
352335
<Entry
353336
id=""

0 commit comments

Comments
 (0)