Merge pull request #32 from EbookFoundation/feature/push-filters

feature/push filters
pull/34/head
Theodore Kluge 2018-11-19 18:45:08 -05:00 committed by GitHub
commit 6d8273b795
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 201 additions and 40 deletions

View File

@ -22,12 +22,21 @@ module.exports = {
try { try {
const id = req.param('id') const id = req.param('id')
const value = req.param('url') const value = req.param('url')
const author = req.param('author')
const publisher = req.param('publisher')
const title = req.param('title')
const isbn = req.param('isbn')
if (value.length) { if (value.length) {
const url = await TargetUrl.update({ id, user: req.user.id }, { url: value }).fetch() const url = await TargetUrl.update({ id, user: req.user.id }, {
url: value,
author,
publisher,
title,
isbn
}).fetch()
return res.json(url) return res.json(url)
} else { } else {
await TargetUrl.destroyOne({ id }) return new HttpError(400, 'URL cannot be blank.').send(res)
return res.status(204).send()
} }
} catch (e) { } catch (e) {
return (new HttpError(500, e.message)).send(res) return (new HttpError(500, e.message)).send(res)

View File

@ -11,6 +11,10 @@ module.exports = {
}, },
url: { url: {
type: 'string' type: 'string'
} },
author: 'string',
publisher: 'string',
title: 'string',
isbn: 'string'
} }
} }

View File

@ -8,6 +8,7 @@ const ACTIONS = {
edit_url: 'edit_url', edit_url: 'edit_url',
delete_url: 'delete_url', delete_url: 'delete_url',
list_url: 'list_url', list_url: 'list_url',
set_editing: 'set_editing',
error: 'error' error: 'error'
} }
@ -28,10 +29,16 @@ export const addUrl = url => ({
data: url data: url
}) })
export const changeUrlField = (id, value) => ({ export const setEditing = id => ({
type: ACTIONS.set_editing,
data: id
})
export const changeUrlField = (id, what, value) => ({
type: ACTIONS.edit_url, type: ACTIONS.edit_url,
data: { data: {
id, id,
what,
value value
} }
}) })
@ -90,13 +97,14 @@ export const createNewUrl = () => async (dispatch, getState) => {
} }
} }
export const setUrl = (id, value) => async (dispatch, getState) => { export const setUrl = (value) => async (dispatch, getState) => {
dispatch(setWorking(true)) dispatch(setWorking(true))
try { try {
await Ajax.patch({ await Ajax.patch({
url: '/api/targets/' + id, url: '/api/targets/' + value.id,
data: { data: {
url: value ...value,
id: undefined
} }
}) })
} catch (e) { } catch (e) {

View File

@ -4,25 +4,96 @@ import React from 'react'
import IconButton from '../components/IconButton' import IconButton from '../components/IconButton'
import UnderlineInput from '../components/UnderlineInput' import UnderlineInput from '../components/UnderlineInput'
import '../../styles/shared/urilistitem.scss' import '../../styles/shared/urilistitem.scss'
import { changeUrlField, setUrl, removeUrl } from '../actions/targets' import { changeUrlField, setUrl, removeUrl, setEditing } from '../actions/targets'
const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i const uriRegex = /(.+:\/\/)?(.+\.)*(.+\.).{1,}(:\d+)?(.+)?/i
const isbnRegex = /^(97(8|9))?\d{9}(\d|X)$/
class UriListItem extends React.Component { class UriListItem extends React.Component {
constructor () {
super()
this.getView = this.getView.bind(this)
this.getEditing = this.getEditing.bind(this)
this.cancelEvent = this.cancelEvent.bind(this)
}
cancelEvent (e, id) {
e.stopPropagation()
if (id === false) return
this.props.dispatch(setEditing(id))
}
getView () {
return (
<li className='uri-list-item flex-container' onClick={(e) => this.cancelEvent(e, this.props.item.id)}>
<div className='stack flex flex-container flex-vertical'>
<span className='label'>Destination URL</span>
<span className='value'>{this.props.item.url}</span>
</div>
<div className='stack flex flex-container flex-vertical'>
<span className='label'>Filters</span>
<span className='value'>{['publisher', 'title', 'author', 'isbn'].reduce((a, x) => a + (this.props.item[x] ? 1 : 0), 0) || 'None'}</span>
</div>
<IconButton icon='delete' onClick={() => this.props.dispatch(removeUrl(this.props.item.id))} />
</li>
)
}
getEditing () {
return (
<li className='uri-list-item flex-container flex-vertical editing' onClick={(e) => this.cancelEvent(e, false)}>
<header className='flex-container' onClick={(e) => this.cancelEvent(e, null)}>
<h3 className='flex'>Editing: {this.props.item.url}</h3>
<IconButton icon='delete' onClick={() => this.props.dispatch(removeUrl(this.props.item.id))} />
</header>
<div className='settings'>
<UnderlineInput
className='uri flex'
type='text'
name={'url-' + this.props.item.id}
placeholder='Destination URL'
value={'' + this.props.item.url}
pattern={uriRegex}
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'url', e.target.value))}
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
<h4>Filters</h4>
<UnderlineInput
className='uri flex'
type='text'
name={'title-' + this.props.id}
placeholder='Ebook title'
value={'' + this.props.item.title}
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'title', e.target.value))}
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
<UnderlineInput
className='uri flex'
type='text'
name={'author-' + this.props.item.id}
placeholder='Author'
value={'' + this.props.item.author}
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'author', e.target.value))}
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
<UnderlineInput
className='uri flex'
type='text'
name={'publisher-' + this.props.item.id}
placeholder='Publisher URL'
value={'' + this.props.item.publisher}
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'publisher', e.target.value))}
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
<UnderlineInput
className='uri flex'
type='number'
name={'isbn-' + this.props.item.id}
placeholder='ISBN'
value={'' + this.props.item.isbn}
pattern={isbnRegex}
onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'isbn', e.target.value))}
onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} />
</div>
</li>
)
}
render () { render () {
return ( return (
<li className='uri-list-item flex-container'> this.props.editing ? this.getEditing() : this.getView()
<UnderlineInput
className='uri flex'
type='text'
name={'url-' + this.props.id}
placeholder='Destination URL'
value={'' + this.props.url}
pattern={uriRegex}
onChange={(e) => this.props.dispatch(changeUrlField(this.props.id, e.target.value))}
onBlur={(e) => this.props.dispatch(setUrl(this.props.id, e.target.value))} />
<IconButton icon='delete' onClick={() => this.props.dispatch(removeUrl(this.props.id))} />
</li>
) )
} }
} }

View File

@ -26,10 +26,14 @@ const reducer = (state = {}, action) => {
} }
case Actions.edit_url: case Actions.edit_url:
urls = state.urls urls = state.urls
urls.find(x => x.id === data.id).url = data.value urls.find(x => x.id === data.id)[data.what] = data.value
return { return {
urls: urls urls: urls
} }
case Actions.set_editing:
return {
editingUrl: data
}
case Actions.error: case Actions.error:
return { return {
error: data.message error: data.message

View File

@ -5,7 +5,7 @@ import ReactDOM from 'react-dom'
import Progress from './components/Progress' import Progress from './components/Progress'
import UriListItem from './containers/UriListItem' import UriListItem from './containers/UriListItem'
import reducer from './reducers/targets' import reducer from './reducers/targets'
import { fetchUrls, createNewUrl } from './actions/targets' import { fetchUrls, createNewUrl, setEditing } from './actions/targets'
import '../styles/targets.scss' import '../styles/targets.scss'
class App extends React.Component { class App extends React.Component {
@ -17,10 +17,8 @@ class App extends React.Component {
email: '', email: '',
password: '' password: ''
}, },
urls: [{ urls: [],
id: 1, editingUrl: null,
url: 'http'
}],
working: false working: false
} }
@ -47,13 +45,13 @@ class App extends React.Component {
return (<UriListItem return (<UriListItem
key={i} key={i}
dispatch={this.dispatch} dispatch={this.dispatch}
id={item.id} item={item}
url={item.url} />) editing={this.state.editingUrl === item.id} />)
}) })
} }
render () { render () {
return ( return (
<div className='root-container flex-container'> <div className='root-container flex-container' onClick={() => this.dispatch(setEditing(null))}>
<aside className='nav nav-left'> <aside className='nav nav-left'>
<header> <header>
<h1>RoE</h1> <h1>RoE</h1>

View File

@ -58,6 +58,13 @@
} }
} }
&:focus + .reacts-to, &:focus + .reacts-to,
&:active + .reacts-to {
.underline:before {
width: 100%;
left: 0;
}
}
&:focus + .reacts-to,
&:active + .reacts-to, &:active + .reacts-to,
&.has-content + .reacts-to { &.has-content + .reacts-to {
label { label {
@ -66,10 +73,6 @@
line-height: 14px; line-height: 14px;
color: $accent-2; color: $accent-2;
} }
.underline:before {
width: 100%;
left: 0;
}
} }
&.invalid { &.invalid {
color: $red; color: $red;

View File

@ -1,12 +1,58 @@
@import '../lib/vars'; @import '../lib/vars';
.uri-list-item { .uri-list-item {
height: 60px; min-height: 60px;
line-height: 60px; line-height: 60px;
background: $background-1; background: white;
box-shadow: $shadow-0; box-shadow: $shadow-0;
padding: 0 0 0 14px;
cursor: pointer;
transition: margin 0.15s $transition,
padding 0.15s $transition,
border-radius 0.15s $transition;
& > .button.icon { .button.icon {
margin: 7px 5px 3px 5px; margin: 7px 5px 3px 5px;
} }
&.editing {
margin: 24px 8px;
line-height: initial;
padding: 10px 0 10px 14px;
border-radius: 3px;
cursor: default;
header {
h3 {
display: inline-block;
margin: 10px 0 0 0;
font-weight: normal;
cursor: pointer;
}
}
.settings {
padding: 0 14px 0 0;
}
h4 {
margin: 30px 0 0 0;
font-weight: normal;
}
}
.stack {
line-height: 25px;
padding: 5px 0;
span {
display: inline-block;
width: 100%;
}
span.label {
font-size: 0.75rem;
color: $black-2;
}
span.value {
margin-top: -4px;
}
}
} }

View File

@ -20,7 +20,7 @@
padding: 0 14px; padding: 0 14px;
margin: -14px 0 8px 0; margin: -14px 0 8px 0;
} }
header { & > header {
padding: 0 14px; padding: 0 14px;
h1 { h1 {
@ -37,10 +37,10 @@
} }
} }
.list { .list {
margin: 14px; margin: 20px 14px;
padding: 0; padding: 0;
list-style: none; list-style: none;
overflow: hidden; // overflow: hidden;
} }
&.working { &.working {
& > .progress { & > .progress {

View File

@ -0,0 +1,18 @@
exports.up = function (knex, Promise) {
return Promise.all([
knex.schema.table('targeturl', t => {
t.string('title')
t.string('author')
t.string('publisher')
t.string('isbn')
})
])
}
exports.down = function (knex, Promise) {
return Promise.all([
knex.schema.table('targeturl', t => {
t.dropColumns('title', 'author', 'publisher', 'isbn')
})
])
}