diff --git a/app/actions/ui.ts b/app/actions/ui.ts index 3aad6e3d..bc68b4eb 100644 --- a/app/actions/ui.ts +++ b/app/actions/ui.ts @@ -10,7 +10,8 @@ import { UI_SET_MODAL, UI_SIGNOUT, UI_HIDE_COMMIT_NUDGE, - UI_SET_DATASET_DIR_PATH + UI_SET_DATASET_DIR_PATH, + UI_SET_EXPORT_PATH } from '../reducers/ui' import { ToastType } from '../models/store' @@ -82,3 +83,10 @@ export const setDatasetDirPath = (path: string) => { path } } + +export const setExportPath = (path: string) => { + return { + type: UI_SET_EXPORT_PATH, + path + } +} diff --git a/app/components/App.tsx b/app/components/App.tsx index 68f499a6..0e45dad0 100644 --- a/app/components/App.tsx +++ b/app/components/App.tsx @@ -10,6 +10,7 @@ import Toast from './Toast' import AppError from './AppError' import AppLoading from './AppLoading' import AddDataset from './modals/AddDataset' +import ExportVersion from './modals/ExportVersion' import LinkDataset from './modals/LinkDataset' import RemoveDataset from './modals/RemoveDataset' import CreateDataset from './modals/CreateDataset' @@ -44,6 +45,8 @@ export interface AppProps { toast: IToast modal: Modal workingDataset: Dataset + exportPath: string + setExportPath: (path: string) => Action children: JSX.Element[] | JSX.Element bootstrap: () => Promise fetchMyDatasets: (page?: number, pageSize?: number) => Promise @@ -183,6 +186,22 @@ class App extends React.Component { break } + case ModalType.ExportVersion: { + modalComponent = ( + setModal(noModalObject)} + /> + ) + break + } + case ModalType.LinkDataset: { const { peername, name } = this.props.workingDataset modalComponent = ( diff --git a/app/components/DatasetSidebar.tsx b/app/components/DatasetSidebar.tsx index 57ed6cf1..c5ef0039 100644 --- a/app/components/DatasetSidebar.tsx +++ b/app/components/DatasetSidebar.tsx @@ -15,7 +15,8 @@ import Spinner from './chrome/Spinner' import { WorkingDataset, ComponentType, Selections } from '../models/store' import ContextMenuArea from 'react-electron-contextmenu' -import { MenuItemConstructorOptions, ipcRenderer } from 'electron' +import { MenuItemConstructorOptions } from 'electron' +import { ModalType, Modal } from '../models/modals' interface HistoryListItemProps { path: string @@ -48,6 +49,7 @@ export interface DatasetSidebarProps { selections: Selections workingDataset: WorkingDataset hideCommitNudge: boolean + setModal: (modal: Modal) => void setActiveTab: (activeTab: string) => Action setSelectedListItem: (type: ComponentType, activeTab: string) => Action fetchWorkingHistory: (page?: number, pageSize?: number) => ApiActionThunk @@ -64,7 +66,8 @@ const DatasetSidebar: React.FunctionComponent = (props) => setSelectedListItem, fetchWorkingHistory, discardChanges, - setHideCommitNudge + setHideCommitNudge, + setModal } = props const { fsiPath, history, status, components } = workingDataset @@ -179,7 +182,14 @@ const DatasetSidebar: React.FunctionComponent = (props) => { label: 'Export this version', click: () => { - ipcRenderer.send('export', `http://localhost:2503/export/${peername}/${name}/at/${path}?download=true&all=true`) + setModal({ + type: ModalType.ExportVersion, + peername: peername || '', + name: name || '', + path: path || '', + timestamp: timestamp, + title: title + }) } } ] diff --git a/app/components/modals/ExportVersion.tsx b/app/components/modals/ExportVersion.tsx new file mode 100644 index 00000000..b4640f93 --- /dev/null +++ b/app/components/modals/ExportVersion.tsx @@ -0,0 +1,134 @@ +import * as React from 'react' +import { Action } from 'redux' +import { remote, ipcRenderer } from 'electron' + +import Modal from './Modal' +import TextInput from '../form/TextInput' +import Buttons from './Buttons' +import moment from 'moment' +import ButtonInput from '../form/ButtonInput' + +interface ExportVersionProps { + onDismissed: () => void + peername: string + name: string + path: string + title: string + exportPath: string + timestamp: Date + setExportPath: (path: string) => Action +} + +const ExportVersion: React.FunctionComponent = (props) => { + const { + onDismissed, + peername, + name, + path, + title, + timestamp, + exportPath: persistedExportPath, + setExportPath: saveExportPath + } = props + + const pathUrl = path === '' ? '' : `/at/${path}` + const exportUrl = `http://localhost:2503/export/${peername}/${name}${pathUrl}?download=true&all=true` + const [exportPath, setExportPath] = React.useState(persistedExportPath) + const [dismissable, setDismissable] = React.useState(true) + const [buttonDisabled, setButtonDisabled] = React.useState(true) + + const handleSubmit = () => { + ipcRenderer.send('export', { url: exportUrl, directory: exportPath }) + onDismissed() + } + + React.useEffect(() => { + // persist the exportPath + if (exportPath !== '') { + saveExportPath(exportPath) + if (buttonDisabled) setButtonDisabled(false) + } else { + setButtonDisabled(true) + } + }, [exportPath]) + + const handleChanges = (name: string, value: any) => { + if (value[value.length - 1] === ' ') { + + } + } + + const handlePathPickerDialog = (showFunc: () => void) => { + new Promise(resolve => { + setDismissable(false) + resolve() + }) + .then(() => showFunc()) + .then(() => setDismissable(true)) + } + + const showDirectoryPicker = () => { + const window = remote.getCurrentWindow() + const directory: string[] | undefined = remote.dialog.showOpenDialog(window, { + properties: ['createDirectory', 'openDirectory'] + }) + + if (!directory) { + return + } + + const selectedPath = directory[0] + + setExportPath(selectedPath) + } + + return ( + {}} + dismissable={dismissable} + setDismissable={setDismissable} + > +
+
+
+
+

{peername}/{name}

+

{title} - {moment(timestamp).format('MMMM Do YYYY, h:mm:ss a')}

+
+
+ +
handlePathPickerDialog(showDirectoryPicker)} >Choose...
+
+
+
+

+ {!buttonDisabled && ( + Qri will save a zip of this dataset version to {exportPath} + )} +

+
+ +
+ ) +} + +export default ExportVersion diff --git a/app/containers/AppContainer.tsx b/app/containers/AppContainer.tsx index 7451e9b3..739d27b5 100644 --- a/app/containers/AppContainer.tsx +++ b/app/containers/AppContainer.tsx @@ -18,7 +18,8 @@ import { setQriCloudAuthenticated, closeToast, setModal, - setDatasetDirPath + setDatasetDirPath, + setExportPath } from '../actions/ui' import { @@ -41,7 +42,7 @@ const AppContainer = connect( const loading = connection.apiConnection === 0 || session.isLoading const hasDatasets = myDatasets.value.length !== 0 const { id: sessionID, peername } = session - const { toast, modal, datasetDirPath } = ui + const { toast, modal, datasetDirPath, exportPath } = ui return { hasDatasets, loading, @@ -52,7 +53,8 @@ const AppContainer = connect( modal, workingDataset, apiConnection, - datasetDirPath + datasetDirPath, + exportPath } }, { @@ -67,6 +69,7 @@ const AppContainer = connect( closeToast, pingApi, setWorkingDataset, + setExportPath, setModal, publishDataset, unpublishDataset, diff --git a/app/main.development.js b/app/main.development.js index 0bd25a91..85361253 100644 --- a/app/main.development.js +++ b/app/main.development.js @@ -470,9 +470,9 @@ app.on('ready', () => }) // catch export events - ipcMain.on('export', async (e, exportPath) => { + ipcMain.on('export', async (e, { url, directory }) => { const win = BrowserWindow.getFocusedWindow() - await download(win, exportPath, { saveAs: true }) + await download(win, url, { directory }) }) ipcMain.on('block-menus', (e, blockMenus) => { diff --git a/app/models/modals.ts b/app/models/modals.ts index 6b4a3151..90a6a0d1 100644 --- a/app/models/modals.ts +++ b/app/models/modals.ts @@ -5,7 +5,8 @@ export enum ModalType { LinkDataset, RemoveDataset, PublishDataset, - UnpublishDataset + UnpublishDataset, + ExportVersion } interface CreateDatasetModal { @@ -39,6 +40,15 @@ export interface RemoveDatasetModal { fsiPath: string } +export interface ExportVersionModal { + type: ModalType.ExportVersion + peername: string + name: string + path: string + timestamp: Date + title: string +} + export interface HideModal { type: ModalType.NoModal } @@ -50,3 +60,4 @@ export type Modal = CreateDatasetModal | PublishDataset | UnpublishDataset | HideModal +| ExportVersionModal diff --git a/app/models/store.ts b/app/models/store.ts index f372ad59..98313c76 100644 --- a/app/models/store.ts +++ b/app/models/store.ts @@ -63,6 +63,7 @@ export interface UI { blockMenus: boolean hideCommitNudge: boolean datasetDirPath: string + exportPath: string } export type SelectedComponent = 'meta' | 'body' | 'schema' | '' diff --git a/app/reducers/ui.ts b/app/reducers/ui.ts index cafe2fcf..9973c573 100644 --- a/app/reducers/ui.ts +++ b/app/reducers/ui.ts @@ -14,7 +14,8 @@ export const UI_CLOSE_TOAST = 'UI_CLOSE_TOAST' export const UI_SET_MODAL = 'UI_SET_MODAL' export const UI_SIGNOUT = 'UI_SIGNOUT' export const UI_HIDE_COMMIT_NUDGE = 'UI_HIDE_COMMIT_NUDGE' -export const UI_SET_DATASET_DIR_PATH = 'UI_SET_DATASET_PATH' +export const UI_SET_DATASET_DIR_PATH = 'UI_SET_DATASET_DIR_PATH' +export const UI_SET_EXPORT_PATH = 'UI_SET_EXPORT_PATH' export const UNAUTHORIZED = 'UNAUTHORIZED' @@ -47,7 +48,8 @@ const initialState = { toast: defaultToast, blockMenus: true, hideCommitNudge: store().getItem(hideCommitNudge) === 'true', - datasetDirPath: store().getItem('datasetDirPath') || '' + datasetDirPath: store().getItem('datasetDirPath') || '', + exportPath: store().getItem('exportPath') || '' } // send an event to electron to block menus on first load @@ -171,6 +173,13 @@ export default (state = initialState, action: AnyAction) => { datasetDirPath: action.path } + case UI_SET_EXPORT_PATH: + store().setItem('exportPath', action.path) + return { + ...state, + exportPath: action.path + } + case ADD_SUCC: case INIT_SUCC: const { peername, name } = action.payload.data