Skip to content

Commit

Permalink
feat(save): set up POST api call
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswhong committed Jul 25, 2019
1 parent 993633f commit f608981
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 37 deletions.
37 changes: 32 additions & 5 deletions app/actions/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function fetchWorkingDataset (): ApiActionThunk {
[CALL_API]: {
endpoint: 'dataset',
method: 'GET',
params: {
segments: {
peername: selections.peername,
name: selections.name
},
Expand All @@ -95,7 +95,7 @@ export function fetchCommitDetail (): ApiActionThunk {
[CALL_API]: {
endpoint: 'dataset',
method: 'GET',
params: {
segments: {
peername: selections.peername,
name: selections.name,
path: commit
Expand All @@ -121,7 +121,7 @@ export function fetchWorkingHistory (): ApiActionThunk {
[CALL_API]: {
endpoint: 'history',
method: 'GET',
params: {
segments: {
peername: selections.peername,
name: selections.name
},
Expand Down Expand Up @@ -150,7 +150,7 @@ export function fetchWorkingStatus (): ApiActionThunk {
[CALL_API]: {
endpoint: 'status',
method: 'GET',
params: {
segments: {
peername: selections.peername,
name: selections.name
},
Expand Down Expand Up @@ -180,7 +180,7 @@ export function fetchBody (): ApiActionThunk {
[CALL_API]: {
endpoint: 'body',
method: 'GET',
params: {
segments: {
peername,
name,
path
Expand All @@ -194,3 +194,30 @@ export function fetchBody (): ApiActionThunk {
return dispatch(action)
}
}

export function saveWorkingDataset (title: string, message: string): ApiActionThunk {
return async (dispatch, getState) => {
const { workingDataset } = getState()
const { peername, name } = workingDataset
const action = {
type: 'save',
[CALL_API]: {
endpoint: 'save',
method: 'POST',
segments: {
peername,
name
},
params: {
fsi: true
},
body: {
title,
message
}
}
}

return dispatch(action)
}
}
4 changes: 3 additions & 1 deletion app/components/Dataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export default class Dataset extends React.Component<DatasetProps> {
}
}

const linkButton = workingDataset.linkpath ? (
const isLinked = !!workingDataset.linkpath
const linkButton = isLinked ? (
<div
className='header-column'
onClick={() => { shell.openItem(String(workingDataset.linkpath)) }}
Expand Down Expand Up @@ -184,6 +185,7 @@ export default class Dataset extends React.Component<DatasetProps> {
maximumWidth={495}
>
<DatasetSidebar
isLinked={isLinked}
activeTab={activeTab}
selectedComponent={selectedComponent}
selectedCommit={selectedCommit}
Expand Down
11 changes: 8 additions & 3 deletions app/components/DatasetSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import { Action } from 'redux'
import classNames from 'classnames'
import moment from 'moment'
import SaveForm from './SaveForm'
import SaveFormContainer from '../containers/SaveFormContainer'

import { WorkingDataset } from '../models/store'

Expand Down Expand Up @@ -88,6 +88,7 @@ interface DatasetSidebarProps {
selectedCommit: string
history: WorkingDataset['history']
status: WorkingDataset['status']
isLinked: boolean
onTabClick: (activeTab: string) => Action
onListItemClick: (type: componentType, activeTab: string) => Action
}
Expand Down Expand Up @@ -115,7 +116,8 @@ const DatasetSidebar: React.FunctionComponent<DatasetSidebarProps> = (props: Dat
history,
status,
onTabClick,
onListItemClick
onListItemClick,
isLinked
} = props

const historyLoaded = !!history
Expand Down Expand Up @@ -193,7 +195,10 @@ const DatasetSidebar: React.FunctionComponent<DatasetSidebarProps> = (props: Dat
)
}
</div>
<SaveForm />
{
isLinked && <SaveFormContainer />
}

</div>
)
}
Expand Down
55 changes: 50 additions & 5 deletions app/components/SaveForm.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,62 @@
import * as React from 'react'
import classNames from 'classnames'
import { ApiAction } from '../store/api'

interface SaveFormProps {
saveWorkingDataset: (title: string, message: string) => Promise<ApiAction>
}

interface SaveFormState {
title: string
message: string
[key: string]: string
}

export default class SaveForm extends React.Component<SaveFormProps, SaveFormState> {
constructor (props: any) {
super(props)
this.state = {
title: '',
message: ''
}

this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}

handleChange (e: any) {
const { name, value } = e.target
this.setState({ [name]: value })
}

handleSubmit (event: any) {
const { title, message } = this.state
this.props.saveWorkingDataset(title, message)
event.preventDefault()
}

export default class SaveForm extends React.Component<{}> {
render () {
const { title } = this.state
const valid = title.length > 3
return (
<form id='save-form'>
<form id='save-form' onSubmit={this.handleSubmit}>
<div className='title'>
<input type='text' name='title' placeholder='Commit message' />
<input
type='text'
name='title'
onChange={this.handleChange}
placeholder='Commit message'
/>
</div>
<div className='message'>
<textarea name='message' placeholder='Detailed description' />
<textarea
name='message'
onChange={this.handleChange}
placeholder='Detailed description'
/>
</div>
<div className='submit'>
<input className='submit'type="submit" value="Submit" />
<input className={classNames('submit', { 'disabled': !valid })} type="submit" value="Submit" />
</div>
</form>
)
Expand Down
9 changes: 9 additions & 0 deletions app/containers/SaveFormContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { connect } from 'react-redux'
import SaveForm from '../components/SaveForm'
import { saveWorkingDataset } from '../actions/api'

const mapStateToProps = () => ({})

const actions = { saveWorkingDataset }

export default connect(mapStateToProps, actions)(SaveForm)
6 changes: 5 additions & 1 deletion app/scss/_saveform.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
$sidebar-item-padding: 6px 10px;

#save-form {
background-color: steelblue;
background-color: #ececec;
height: 180px;
display: flex;
flex-direction: column;
Expand All @@ -29,5 +29,9 @@ $sidebar-item-padding: 6px 10px;

.submit {
padding: $sidebar-item-padding;

&.disabled {
color: #ccc
}
}
}
72 changes: 50 additions & 22 deletions app/store/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export interface ApiAction extends AnyAction {
// method is the HTTP method used
method: 'GET' | 'PUT' | 'POST' | 'DELETE'
// params is a list of parameters used to construct the API request
params?: ApiQueryParams
segments?: ApiSegments
// query is an object of query parameters to be appended to the API call URL
query?: object
// body is the body for POST requests
body?: object|[]
// map is a function
// map defaults to the identity function
map?: (data: object|[]) => any
Expand All @@ -33,14 +37,18 @@ const identityFunc = <T>(a: T): T => a

// ApiQueryParams is an interface for all possible query parameters passed to
// the API
export interface ApiQueryParams {
export interface ApiSegments {
peername?: string
name?: string
peerID?: string
path?: string

page?: number
pageSize?: number
fsi?: boolean
}

interface ApiParams {
[key: string]: string
}

// ApiActionThunk is the return value of an Api action.
Expand Down Expand Up @@ -81,8 +89,8 @@ export function apiActionTypes (endpoint: string): [string, string, string] {
}

// getJSON fetches json data from a url
async function getJSON<T> (url: string): Promise<T> {
const res = await fetch(url)
async function getJSON<T> (url: string, options: FetchOptions): Promise<T> {
const res = await fetch(url, options)
if (res.status !== 200) {
throw new Error(`Received non-200 status code: ${res.status}`)
}
Expand All @@ -98,59 +106,79 @@ const endpointMap: Record<string, string> = {
'dataset': '', // dataset endpoints are constructured through query param values
'body': 'body', // dataset endpoints are constructured through query param values
'history': 'history',
'status': 'dsstatus'
'status': 'dsstatus',
'save': 'save'
}

function apiUrl (endpoint: string, params?: ApiQueryParams): [string, string] {
function apiUrl (endpoint: string, segments?: ApiSegments, params?: ApiParams): [string, string] {
const path = endpointMap[endpoint]
if (path === undefined) {
return ['', `${endpoint} is not a valid api endpoint`]
}

let url = `http://localhost:2503/${path}`
if (!params) {
if (!segments) {
return [url, '']
}

if (params.peername) {
url += `/${params.peername}`
if (segments.peername) {
url += `/${segments.peername}`
}
if (params.name) {
url += `/${params.name}`
if (segments.name) {
url += `/${segments.name}`
}
if (params.peerID || params.path) {
if (segments.peerID || segments.path) {
url += '/at'
}
if (params.peerID) {
url += `/${params.peerID}`
if (segments.peerID) {
url += `/${segments.peerID}`
}
if (params.path) {
url += params.path
if (segments.path) {
url += segments.path
}

if (params) {
url += '?'
Object.keys(params).forEach((key) => {
url += `${key}=${params[key]}`
})
}
return [url, '']
}

interface FetchOptions {
method: string
body?: string
}

// getAPIJSON constructs an API url & fetches a JSON response
async function getAPIJSON<T> (endpoint: string, params?: ApiQueryParams): Promise<T> {
const [url, err] = apiUrl(endpoint, params)
async function getAPIJSON<T> (
endpoint: string,
method: string,
segments?: ApiSegments,
params?: ApiParams,
body?: object|[]
): Promise<T> {
const [url, err] = apiUrl(endpoint, segments, params)
if (err) {
throw err
}
return getJSON(url)
const options: FetchOptions = { method }
if (body) options.body = JSON.stringify(body)
return getJSON(url, options)
}

// apiMiddleware manages requests to the qri JSON API
export const apiMiddleware: Middleware = () => (next: Dispatch<AnyAction>) => async (action: any): Promise<any> => {
if (action[CALL_API]) {
let data: APIResponseEnvelope
let { endpoint = '', map = identityFunc, params } = action[CALL_API]
let { endpoint = '', method, map = identityFunc, segments, params, body } = action[CALL_API]
const [REQ_TYPE, SUCC_TYPE, FAIL_TYPE] = apiActionTypes(action.type)

next({ type: REQ_TYPE })

try {
data = await getAPIJSON(endpoint, params)
data = await getAPIJSON(endpoint, method, segments, params, body)
} catch (err) {
return next({
type: FAIL_TYPE,
Expand Down

0 comments on commit f608981

Please sign in to comment.