Skip to content

Commit

Permalink
feat: link to filesystem (checkout)
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswhong committed Aug 29, 2019
1 parent 9ae00fd commit db3ae89
Show file tree
Hide file tree
Showing 12 changed files with 279 additions and 48 deletions.
44 changes: 43 additions & 1 deletion app/actions/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Dataset, Commit } from '../models/dataset'
import { openToast } from './ui'
import { setWorkingDataset, setSelectedListItem } from './selections'

import { RESET_MY_DATASETS } from '../reducers/myDatasets'

const pageSizeDefault = 15
const bodyPageSizeDefault = 100

Expand Down Expand Up @@ -48,8 +50,18 @@ export function fetchWorkingDatasetDetails (): ApiActionThunk {
}
}

export function fetchMyDatasets (page: number = 1, pageSize: number = pageSizeDefault): ApiActionThunk {
// clears the dataset list
function resetMyDatasets (): Action {
return {
type: RESET_MY_DATASETS
}
}

export function fetchMyDatasets (page: number = 1, pageSize: number = pageSizeDefault, invalidatePagination: boolean = false): ApiActionThunk {
return async (dispatch, getState) => {
if (invalidatePagination) {
dispatch(resetMyDatasets())
}
const state = getState()
if (page !== 1 &&
state &&
Expand Down Expand Up @@ -473,3 +485,33 @@ export function unpublishDataset (dataset: WorkingDataset): ApiActionThunk {
return dispatch(openToast('success', 'dataset unpublished'))
}
}

export function linkDataset (peername: string, name: string, dir: string): ApiActionThunk {
return async (dispatch, getState) => {
const whenOk = chainSuccess(dispatch, getState)
let response: Action

try {
const action = {
type: 'checkout',
[CALL_API]: {
endpoint: 'checkout',
method: 'POST',
segments: {
peername,
name
},
params: {
dir
}
}
}
response = await dispatch(action)
response = await whenOk(fetchWorkingDatasetDetails())(response)
response = await whenOk(fetchMyDatasets(1, pageSizeDefault, true))(response) // non-paginated
} catch (action) {
throw action
}
return response
}
}
23 changes: 21 additions & 2 deletions app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import { ipcRenderer } from 'electron'
import Toast from './Toast'
import AppError from './AppError'
import AppLoading from './AppLoading'
import AddDataset from './modals/AddDataset'
import LinkDataset from './modals/LinkDataset'
import CreateDataset from './modals/CreateDataset'
import RoutesContainer from '../containers/RoutesContainer'
import AddDataset from './modals/AddDataset'

// import models
import { ApiAction } from '../store/api'
import { Modal, ModalType, NoModal } from '../models/modals'
import { Toast as IToast } from '../models/store'
import { Dataset } from '../models/dataset'

export const QRI_CLOUD_ROOT = 'https://qri.cloud'

Expand All @@ -28,10 +30,12 @@ export interface AppProps {
qriCloudAuthenticated: boolean
toast: IToast
modal: Modal
workingDataset: Dataset
children: JSX.Element[] | JSX.Element
fetchSession: () => Promise<ApiAction>
fetchMyDatasets: (page?: number, pageSize?: number) => Promise<ApiAction>
addDataset: (peername: string, name: string) => Promise<ApiAction>
linkDataset: (peername: string, name: string, dir: string) => Promise<ApiAction>
setWorkingDataset: (peername: string, name: string, isLinked: boolean) => Promise<ApiAction>
initDataset: (path: string, name: string, format: string) => Promise<ApiAction>
acceptTOS: () => Action
Expand Down Expand Up @@ -97,7 +101,8 @@ export default class App extends React.Component<AppProps, AppState> {
}

private renderModal (): JSX.Element | null {
const { modal, setModal } = this.props
const { modal, setModal, workingDataset } = this.props
const { peername, name } = workingDataset
const Modal = modal

if (!Modal) return null
Expand Down Expand Up @@ -131,6 +136,20 @@ export default class App extends React.Component<AppProps, AppState> {
fetchMyDatasets={this.props.fetchMyDatasets}
/>
</CSSTransition>
<CSSTransition
in={ModalType.LinkDataset === Modal.type}
classNames='fade'
component='div'
timeout={300}
unmountOnExit
>
<LinkDataset
peername={peername}
name={name}
onSubmit={this.props.linkDataset}
onDismissed={async () => setModal(NoModal)}
/>
</CSSTransition>
</div>
)
}
Expand Down
1 change: 1 addition & 0 deletions app/components/CommitDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ const CommitDetails: React.FunctionComponent<CommitDetailsProps> = ({
selectedComponent={selectedComponent}
selectionType={'commitComponent' as ComponentType}
onComponentClick={setSelectedListItem}
isLinked
/>
</Resizable>
<div className='content-wrapper'>
Expand Down
33 changes: 16 additions & 17 deletions app/components/Dataset.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
import * as React from 'react'
import classNames from 'classnames'
import { Action } from 'redux'
import { ipcRenderer, shell } from 'electron'
import classNames from 'classnames'
import ReactTooltip from 'react-tooltip'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFile } from '@fortawesome/free-regular-svg-icons'
import { ipcRenderer, shell } from 'electron'
import { CSSTransition } from 'react-transition-group'
import { faLink } from '@fortawesome/free-solid-svg-icons'
import { faFile } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

import { ApiAction, ApiActionThunk } from '../store/api'
import ExternalLink from './ExternalLink'
import { Resizable } from './Resizable'
import { Session } from '../models/session'
import DatasetSidebar from './DatasetSidebar'
import UnlinkedDataset from './UnlinkedDataset'
import DatasetComponent from './DatasetComponent'
import { QRI_CLOUD_URL } from '../utils/registry'
import { Modal, ModalType } from '../models/modals'
import { defaultSidebarWidth } from '../reducers/ui'
import HeaderColumnButton from './chrome/HeaderColumnButton'
import DatasetListContainer from '../containers/DatasetListContainer'
import CommitDetailsContainer from '../containers/CommitDetailsContainer'
import HeaderColumnButton from './chrome/HeaderColumnButton'
import HeaderColumnButtonDropdown from './chrome/HeaderColumnButtonDropdown'

import { CSSTransition } from 'react-transition-group'
import { Modal } from '../models/modals'

import { defaultSidebarWidth } from '../reducers/ui'

import { QRI_CLOUD_URL } from '../utils/registry'

import {
UI,
Selections,
Expand All @@ -33,8 +31,6 @@ import {
Mutations
} from '../models/store'

import { Session } from '../models/session'

export interface DatasetProps {
// redux state
ui: UI
Expand Down Expand Up @@ -189,10 +185,12 @@ export default class Dataset extends React.Component<DatasetProps> {
activeTab,
component: selectedComponent,
commit: selectedCommit,
isLinked,
published
} = selections

// don't use isLinked from selections
const isLinked = workingDataset.linkpath !== ''

const { history, status, path } = workingDataset

// actions
Expand All @@ -202,6 +200,7 @@ export default class Dataset extends React.Component<DatasetProps> {
setSidebarWidth,
setSelectedListItem,
fetchWorkingHistory,
// linkDataset,
signout
} = this.props

Expand All @@ -221,7 +220,7 @@ export default class Dataset extends React.Component<DatasetProps> {
<FontAwesomeIcon icon={faLink} transform="shrink-8" />
</span>
)}
onClick={() => { console.log('not finished: linking to filesystem') }}
onClick={() => { setModal({ type: ModalType.LinkDataset }) }}
/>
)

Expand Down Expand Up @@ -315,7 +314,7 @@ export default class Dataset extends React.Component<DatasetProps> {
mountOnEnter
unmountOnExit
>
<UnlinkedDataset />
<UnlinkedDataset setModal={setModal}/>
</CSSTransition>
<CSSTransition
in={activeTab === 'status' && isLinked}
Expand Down
13 changes: 11 additions & 2 deletions app/components/UnlinkedDataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import * as React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUnlink } from '@fortawesome/free-solid-svg-icons'

const UnlinkedDataset: React.FunctionComponent = () => (
import { Modal, ModalType } from '../models/modals'

interface UnlinkedDatasetProps {
setModal: (modal: Modal) => void
}

const UnlinkedDataset: React.FunctionComponent<UnlinkedDatasetProps> = ({ setModal }) => (
<div className={'unlinked-dataset'}>
<div className={'message-container'}>
<div>
<h4>This is an Unlinked Dataset</h4>
<p>To use the status tab, link the dataset to your filesystem.</p>
<a href='#'>Link this Dataset</a>
<a href='#' onClick={(e) => {
e.preventDefault()
setModal({ type: ModalType.LinkDataset })
}}>Link this Dataset</a>
</div>
<FontAwesomeIcon icon={faUnlink} color='#777'/>
</div>
Expand Down
145 changes: 145 additions & 0 deletions app/components/modals/LinkDataset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as React from 'react'
import { remote } from 'electron'
import Modal from './Modal'
import { ApiAction } from '../../store/api'
import TextInput from '../form/TextInput'
import Error from './Error'
import Buttons from './Buttons'
import ButtonInput from '../form/ButtonInput'

interface LinkDatasetProps {
peername: string
name: string
onDismissed: () => void
onSubmit: (peername: string, name: string, dir: string) => Promise<ApiAction>
}

const LinkDataset: React.FunctionComponent<LinkDatasetProps> = ({ peername, name, onDismissed, onSubmit }) => {
const [path, setPath] = React.useState('')
const [datasetDirectory, setDatasetDirectory] = React.useState(name)

const [dismissable, setDismissable] = React.useState(true)
const [buttonDisabled, setButtonDisabled] = React.useState(true)
const [alreadyDatasetError, setAlreadyDatasetError] = React.useState('')

React.useEffect(() => {
// if path is empty, disable the button
setButtonDisabled(!path)
}, [path])

// should come from props
const [error, setError] = React.useState('')
const [loading, setLoading] = React.useState(false)

// should come from props/actions that has us check if the directory already contains a qri dataset
const isQriDataset = (path: string) => !path

const showDirectoryPicker = () => {
const window = remote.getCurrentWindow()
const directory: string[] | undefined = remote.dialog.showOpenDialog(window, {
properties: ['createDirectory', 'openDirectory']
})

if (!directory) {
return
}

const path = directory[0]

setPath(path)
const isDataset = isQriDataset(path)
if (isDataset) {
setAlreadyDatasetError('A dataset already exists in this directory.')
setButtonDisabled(true)
}
}

const handlePickerDialog = (showFunc: () => void) => {
new Promise(resolve => {
setDismissable(false)
resolve()
})
.then(() => showFunc())
.then(() => setDismissable(true))
}

const handleChanges = (name: string, value: any) => {
if (value[value.length - 1] === ' ') {
return
}
if (name === 'path') {
setPath(value)
setAlreadyDatasetError('')
}

if (name === 'datasetDirectory') {
setDatasetDirectory(value)
setAlreadyDatasetError('')
}
}

const handleSubmit = () => {
setDismissable(false)
setLoading(true)
error && setError('')
if (!onSubmit) return
onSubmit(peername, name, `${path}/${datasetDirectory}`)
.then(onDismissed)
.catch((action) => {
setLoading(false)
setDismissable(true)
setError(action.payload.err.message)
})
}

return (
<Modal
id="LinkDataset"
title={`Link ${peername}/${name}`}
onDismissed={onDismissed}
onSubmit={() => {}}
dismissable={dismissable}
setDismissable={setDismissable}
>
<div className='content-wrap'>
<div className='content'>
<TextInput
name='datasetDirectory'
label='Dataset Directory'
type=''
value={datasetDirectory}
onChange={handleChanges}
maxLength={600}
errorText={alreadyDatasetError}
/>
<div className='flex-space-between'>
<TextInput
name='path'
label='Save Path'
type=''
value={path}
onChange={handleChanges}
maxLength={600}
errorText={alreadyDatasetError}
/>
<div className='margin-left'><ButtonInput onClick={() => handlePickerDialog(showDirectoryPicker)} >Choose...</ButtonInput></div>
</div>
</div>
</div>
<p className='submit-message'>
{path !== '' && datasetDirectory !== '' && (<span>Qri will create <span>{path}/{datasetDirectory}</span></span>)}
</p>
<Error text={error} />
<Buttons
cancelText='cancel'
onCancel={onDismissed}
submitText='Link Dataset'
onSubmit={handleSubmit}
disabled={buttonDisabled}
loading={loading}
/>
</Modal>
)
}

export default LinkDataset
Loading

0 comments on commit db3ae89

Please sign in to comment.