Skip to content

Commit f38cad7

Browse files
committed
feat(Export Modal): ExportVersion modal component
This also adds `exportPath` to the state tree and local storage, so we remember the last place the user wanted to store and export
1 parent 51544e1 commit f38cad7

File tree

9 files changed

+207
-12
lines changed

9 files changed

+207
-12
lines changed

app/actions/ui.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
UI_SET_MODAL,
1111
UI_SIGNOUT,
1212
UI_HIDE_COMMIT_NUDGE,
13-
UI_SET_DATASET_DIR_PATH
13+
UI_SET_DATASET_DIR_PATH,
14+
UI_SET_EXPORT_PATH
1415
} from '../reducers/ui'
1516

1617
import { ToastType } from '../models/store'
@@ -82,3 +83,10 @@ export const setDatasetDirPath = (path: string) => {
8283
path
8384
}
8485
}
86+
87+
export const setExportPath = (path: string) => {
88+
return {
89+
type: UI_SET_EXPORT_PATH,
90+
path
91+
}
92+
}

app/components/App.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Toast from './Toast'
1010
import AppError from './AppError'
1111
import AppLoading from './AppLoading'
1212
import AddDataset from './modals/AddDataset'
13+
import ExportVersion from './modals/ExportVersion'
1314
import LinkDataset from './modals/LinkDataset'
1415
import RemoveDataset from './modals/RemoveDataset'
1516
import CreateDataset from './modals/CreateDataset'
@@ -44,6 +45,8 @@ export interface AppProps {
4445
toast: IToast
4546
modal: Modal
4647
workingDataset: Dataset
48+
exportPath: string
49+
setExportPath: (path: string) => Action
4750
children: JSX.Element[] | JSX.Element
4851
bootstrap: () => Promise<ApiAction>
4952
fetchMyDatasets: (page?: number, pageSize?: number) => Promise<ApiAction>
@@ -183,6 +186,22 @@ class App extends React.Component<AppProps, AppState> {
183186
break
184187
}
185188

189+
case ModalType.ExportVersion: {
190+
modalComponent = (
191+
<ExportVersion
192+
peername={modal.peername}
193+
name={modal.name}
194+
path={modal.path}
195+
title={modal.title}
196+
timestamp={modal.timestamp}
197+
exportPath={this.props.exportPath}
198+
setExportPath={this.props.setExportPath}
199+
onDismissed={async () => setModal(noModalObject)}
200+
/>
201+
)
202+
break
203+
}
204+
186205
case ModalType.LinkDataset: {
187206
const { peername, name } = this.props.workingDataset
188207
modalComponent = (

app/components/DatasetSidebar.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import Spinner from './chrome/Spinner'
1515

1616
import { WorkingDataset, ComponentType, Selections } from '../models/store'
1717
import ContextMenuArea from 'react-electron-contextmenu'
18-
import { MenuItemConstructorOptions, ipcRenderer } from 'electron'
18+
import { MenuItemConstructorOptions } from 'electron'
19+
import { ModalType, Modal } from '../models/modals'
1920

2021
interface HistoryListItemProps {
2122
path: string
@@ -48,6 +49,7 @@ export interface DatasetSidebarProps {
4849
selections: Selections
4950
workingDataset: WorkingDataset
5051
hideCommitNudge: boolean
52+
setModal: (modal: Modal) => void
5153
setActiveTab: (activeTab: string) => Action
5254
setSelectedListItem: (type: ComponentType, activeTab: string) => Action
5355
fetchWorkingHistory: (page?: number, pageSize?: number) => ApiActionThunk
@@ -64,7 +66,8 @@ const DatasetSidebar: React.FunctionComponent<DatasetSidebarProps> = (props) =>
6466
setSelectedListItem,
6567
fetchWorkingHistory,
6668
discardChanges,
67-
setHideCommitNudge
69+
setHideCommitNudge,
70+
setModal
6871
} = props
6972

7073
const { fsiPath, history, status, components } = workingDataset
@@ -179,7 +182,14 @@ const DatasetSidebar: React.FunctionComponent<DatasetSidebarProps> = (props) =>
179182
{
180183
label: 'Export this version',
181184
click: () => {
182-
ipcRenderer.send('export', `http://localhost:2503/export/${peername}/${name}/at/${path}?download=true&all=true`)
185+
setModal({
186+
type: ModalType.ExportVersion,
187+
peername: peername || '',
188+
name: name || '',
189+
path: path || '',
190+
timestamp: timestamp,
191+
title: title
192+
})
183193
}
184194
}
185195
]
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import * as React from 'react'
2+
import { Action } from 'redux'
3+
import { remote, ipcRenderer } from 'electron'
4+
5+
import Modal from './Modal'
6+
import TextInput from '../form/TextInput'
7+
import Buttons from './Buttons'
8+
import moment from 'moment'
9+
import ButtonInput from '../form/ButtonInput'
10+
11+
interface ExportVersionProps {
12+
onDismissed: () => void
13+
peername: string
14+
name: string
15+
path: string
16+
title: string
17+
exportPath: string
18+
timestamp: Date
19+
setExportPath: (path: string) => Action
20+
}
21+
22+
const ExportVersion: React.FunctionComponent<ExportVersionProps> = (props) => {
23+
const {
24+
onDismissed,
25+
peername,
26+
name,
27+
path,
28+
title,
29+
timestamp,
30+
exportPath: persistedExportPath,
31+
setExportPath: saveExportPath
32+
} = props
33+
34+
const pathUrl = path === '' ? '' : `/at/${path}`
35+
const exportUrl = `http://localhost:2503/export/${peername}/${name}${pathUrl}?download=true&all=true`
36+
const [exportPath, setExportPath] = React.useState(persistedExportPath)
37+
const [dismissable, setDismissable] = React.useState(true)
38+
const [buttonDisabled, setButtonDisabled] = React.useState(true)
39+
40+
const handleSubmit = () => {
41+
ipcRenderer.send('export', { url: exportUrl, directory: exportPath })
42+
onDismissed()
43+
}
44+
45+
React.useEffect(() => {
46+
// persist the exportPath
47+
if (exportPath !== '') {
48+
saveExportPath(exportPath)
49+
if (buttonDisabled) setButtonDisabled(false)
50+
} else {
51+
setButtonDisabled(true)
52+
}
53+
}, [exportPath])
54+
55+
const handleChanges = (name: string, value: any) => {
56+
if (value[value.length - 1] === ' ') {
57+
58+
}
59+
}
60+
61+
const handlePathPickerDialog = (showFunc: () => void) => {
62+
new Promise(resolve => {
63+
setDismissable(false)
64+
resolve()
65+
})
66+
.then(() => showFunc())
67+
.then(() => setDismissable(true))
68+
}
69+
70+
const showDirectoryPicker = () => {
71+
const window = remote.getCurrentWindow()
72+
const directory: string[] | undefined = remote.dialog.showOpenDialog(window, {
73+
properties: ['createDirectory', 'openDirectory']
74+
})
75+
76+
if (!directory) {
77+
return
78+
}
79+
80+
const selectedPath = directory[0]
81+
82+
setExportPath(selectedPath)
83+
}
84+
85+
return (
86+
<Modal
87+
id="export_modal"
88+
title={'Export a Version of this Dataset'}
89+
onDismissed={onDismissed}
90+
onSubmit={() => {}}
91+
dismissable={dismissable}
92+
setDismissable={setDismissable}
93+
>
94+
<div className='content-wrap'>
95+
<div>
96+
<div className='content'>
97+
<div className='margin-bottom'>
98+
<p><strong>{peername}/{name}</strong></p>
99+
<p>{title} - <i>{moment(timestamp).format('MMMM Do YYYY, h:mm:ss a')}</i></p>
100+
</div>
101+
<div className='flex-space-between'>
102+
<TextInput
103+
name='savePath'
104+
label='Export Folder'
105+
labelTooltip='Qri will export the dataset to this folder'
106+
tooltipFor='modal-tooltip'
107+
type=''
108+
value={exportPath}
109+
onChange={handleChanges}
110+
maxLength={600}
111+
/>
112+
<div className='margin-left'><ButtonInput id='chooseSavePath' onClick={() => handlePathPickerDialog(showDirectoryPicker)} >Choose...</ButtonInput></div>
113+
</div>
114+
</div>
115+
</div>
116+
<p className='submit-message'>
117+
{!buttonDisabled && (
118+
<span>Qri will save a zip of this dataset version to {exportPath} </span>
119+
)}
120+
</p>
121+
</div>
122+
<Buttons
123+
cancelText='cancel'
124+
onCancel={onDismissed}
125+
submitText='Export Version'
126+
onSubmit={handleSubmit}
127+
disabled={buttonDisabled}
128+
loading={false}
129+
/>
130+
</Modal>
131+
)
132+
}
133+
134+
export default ExportVersion

app/containers/AppContainer.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
setQriCloudAuthenticated,
1919
closeToast,
2020
setModal,
21-
setDatasetDirPath
21+
setDatasetDirPath,
22+
setExportPath
2223
} from '../actions/ui'
2324

2425
import {
@@ -41,7 +42,7 @@ const AppContainer = connect(
4142
const loading = connection.apiConnection === 0 || session.isLoading
4243
const hasDatasets = myDatasets.value.length !== 0
4344
const { id: sessionID, peername } = session
44-
const { toast, modal, datasetDirPath } = ui
45+
const { toast, modal, datasetDirPath, exportPath } = ui
4546
return {
4647
hasDatasets,
4748
loading,
@@ -52,7 +53,8 @@ const AppContainer = connect(
5253
modal,
5354
workingDataset,
5455
apiConnection,
55-
datasetDirPath
56+
datasetDirPath,
57+
exportPath
5658
}
5759
},
5860
{
@@ -67,6 +69,7 @@ const AppContainer = connect(
6769
closeToast,
6870
pingApi,
6971
setWorkingDataset,
72+
setExportPath,
7073
setModal,
7174
publishDataset,
7275
unpublishDataset,

app/main.development.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,9 +470,9 @@ app.on('ready', () =>
470470
})
471471

472472
// catch export events
473-
ipcMain.on('export', async (e, exportPath) => {
473+
ipcMain.on('export', async (e, { url, directory }) => {
474474
const win = BrowserWindow.getFocusedWindow()
475-
await download(win, exportPath, { saveAs: true })
475+
await download(win, url, { directory })
476476
})
477477

478478
ipcMain.on('block-menus', (e, blockMenus) => {

app/models/modals.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export enum ModalType {
55
LinkDataset,
66
RemoveDataset,
77
PublishDataset,
8-
UnpublishDataset
8+
UnpublishDataset,
9+
ExportVersion
910
}
1011

1112
interface CreateDatasetModal {
@@ -39,6 +40,15 @@ export interface RemoveDatasetModal {
3940
fsiPath: string
4041
}
4142

43+
export interface ExportVersionModal {
44+
type: ModalType.ExportVersion
45+
peername: string
46+
name: string
47+
path: string
48+
timestamp: Date
49+
title: string
50+
}
51+
4252
export interface HideModal {
4353
type: ModalType.NoModal
4454
}
@@ -50,3 +60,4 @@ export type Modal = CreateDatasetModal
5060
| PublishDataset
5161
| UnpublishDataset
5262
| HideModal
63+
| ExportVersionModal

app/models/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface UI {
6363
blockMenus: boolean
6464
hideCommitNudge: boolean
6565
datasetDirPath: string
66+
exportPath: string
6667
}
6768

6869
export type SelectedComponent = 'meta' | 'body' | 'schema' | ''

app/reducers/ui.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export const UI_CLOSE_TOAST = 'UI_CLOSE_TOAST'
1414
export const UI_SET_MODAL = 'UI_SET_MODAL'
1515
export const UI_SIGNOUT = 'UI_SIGNOUT'
1616
export const UI_HIDE_COMMIT_NUDGE = 'UI_HIDE_COMMIT_NUDGE'
17-
export const UI_SET_DATASET_DIR_PATH = 'UI_SET_DATASET_PATH'
17+
export const UI_SET_DATASET_DIR_PATH = 'UI_SET_DATASET_DIR_PATH'
18+
export const UI_SET_EXPORT_PATH = 'UI_SET_EXPORT_PATH'
1819

1920
export const UNAUTHORIZED = 'UNAUTHORIZED'
2021

@@ -47,7 +48,8 @@ const initialState = {
4748
toast: defaultToast,
4849
blockMenus: true,
4950
hideCommitNudge: store().getItem(hideCommitNudge) === 'true',
50-
datasetDirPath: store().getItem('datasetDirPath') || ''
51+
datasetDirPath: store().getItem('datasetDirPath') || '',
52+
exportPath: store().getItem('exportPath') || ''
5153
}
5254

5355
// send an event to electron to block menus on first load
@@ -171,6 +173,13 @@ export default (state = initialState, action: AnyAction) => {
171173
datasetDirPath: action.path
172174
}
173175

176+
case UI_SET_EXPORT_PATH:
177+
store().setItem('exportPath', action.path)
178+
return {
179+
...state,
180+
exportPath: action.path
181+
}
182+
174183
case ADD_SUCC:
175184
case INIT_SUCC:
176185
const { peername, name } = action.payload.data

0 commit comments

Comments
 (0)