diff options
Diffstat (limited to 'src/web')
-rw-r--r-- | src/web/js/components/MiddlePanel.react.js | 12 | ||||
-rw-r--r-- | src/web/js/components/RightPanel.react.js | 252 | ||||
-rw-r--r-- | src/web/js/components/time.react.js | 15 | ||||
-rw-r--r-- | src/web/js/stores/MiddlePanelStore.js | 3 | ||||
-rw-r--r-- | src/web/js/stores/RightPanelStore.js | 4 | ||||
-rw-r--r-- | src/web/models/feed.py | 2 | ||||
-rw-r--r-- | src/web/static/css/one-page-app.css | 29 | ||||
-rw-r--r-- | src/web/views/views.py | 11 |
8 files changed, 265 insertions, 63 deletions
diff --git a/src/web/js/components/MiddlePanel.react.js b/src/web/js/components/MiddlePanel.react.js index 6b3eb427..6bfdaaa9 100644 --- a/src/web/js/components/MiddlePanel.react.js +++ b/src/web/js/components/MiddlePanel.react.js @@ -1,6 +1,4 @@ var React = require('react'); -var ReactIntl = require('react-intl'); -var FormattedRelative = ReactIntl.FormattedRelative; var Row = require('react-bootstrap/Row'); var Button = require('react-bootstrap/Button'); @@ -11,8 +9,9 @@ var MiddlePanelStore = require('../stores/MiddlePanelStore'); var MiddlePanelActions = require('../actions/MiddlePanelActions'); var RightPanelActions = require('../actions/RightPanelActions'); +var JarrTime = require('./time.react'); + var TableLine = React.createClass({ - mixins: [ReactIntl.IntlMixin], propTypes: {article_id: React.PropTypes.number.isRequired, feed_title: React.PropTypes.string.isRequired, icon_url: React.PropTypes.string, @@ -49,11 +48,10 @@ var TableLine = React.createClass({ } // FIXME https://github.com/yahoo/react-intl/issues/189 // use FormattedRelative when fixed, will have to upgrade to ReactIntlv2 - var date = (<time dateTime={this.props.date} title={this.props.date}> - {this.formatRelative(this.props.timestamp)} - </time>); return (<div className={clsses} onClick={this.loadArticle}> - <h5><strong>{title}</strong></h5>{date} + <h5><strong>{title}</strong></h5> + <JarrTime text={this.props.date} + stamp={this.props.timestamp} /> <div>{read} {liked} {this.props.title}</div> </div> ); diff --git a/src/web/js/components/RightPanel.react.js b/src/web/js/components/RightPanel.react.js index a18f0ac3..2e2aac3e 100644 --- a/src/web/js/components/RightPanel.react.js +++ b/src/web/js/components/RightPanel.react.js @@ -1,70 +1,223 @@ var React = require('react'); var Col = require('react-bootstrap/Col'); -var Panel = require('react-bootstrap/Panel'); +var Glyphicon = require('react-bootstrap/Glyphicon'); +var Button = require('react-bootstrap/Button'); +var ButtonGroup = require('react-bootstrap/ButtonGroup'); + var RightPanelStore = require('../stores/RightPanelStore'); +var JarrTime = require('./time.react'); -var Article = React.createClass({ - propTypes: {article: React.PropTypes.object.isRequired}, - render: function() { +var PanelMixin = { + propTypes: {obj: React.PropTypes.object.isRequired}, + getHeader: function() { var icon = null; - if(this.props.article.icon_url){ - icon = (<img width="16px" src={this.props.article.icon_url} />); + if(this.props.obj.icon_url){ + icon = (<img width="16px" src={this.props.obj.icon_url} />); + } + var btn_grp = null; + if(this.isEditable() || this.isRemovable()) { + var edit_button = null; + if(this.isEditable()) { + edit_button = (<Button onClick={this.onClickEdit}> + <Glyphicon glyph="pencil" /> + </Button>); + } + var rem_button = null; + if(this.isRemovable()) { + rem_button = (<Button onClick={this.onClickRemove}> + <Glyphicon glyph="remove-sign" /> + </Button>); + } + btn_grp = (<ButtonGroup bsSize="small"> + {edit_button} + {rem_button} + </ButtonGroup>); } - var header = (<h4> - {icon}<strong>Title:</strong> {this.props.article.title} - </h4>); - return (<Panel header={header}> - <div dangerouslySetInnerHTML={{__html: this.props.article.content}} /> - </Panel> + return (<div id="right-panel-heading" className="panel-heading"> + <h4>{icon}<strong>Title:</strong> {this.getTitle()}</h4> + {btn_grp} + </div>); + }, + getKey: function(prefix, suffix) { + return ((this.state.edit_mode?'edit':'fix') + prefix + + '-' + this.props.obj.id + '-' + suffix); + }, + getCore: function() { + var items = []; + var key; + if(!this.state.edit_mode) { + this.fields.map(function(field) { + key = this.getKey('dt', field.key); + items.push(<dt key={key}>{field.title}</dt>); + key = this.getKey('dd', field.key); + if(field.type == 'string') { + items.push(<dd key={key}>{this.props.obj[field.key]}</dd>); + } else if(field.type == 'bool') { + if(this.props.obj[field.key]) { + items.push(<dd key={key}><Glyphicon glyph="ok" /></dd>); + } else { + items.push(<dd key={key}><Glyphicon glyph="pause" /></dd>); + } + } else if (field.type == 'link') { + items.push(<dd key={key}> + <a href={this.props.obj[field.key]}> + {this.props.obj[field.key]} + </a> + </dd>); + } + }.bind(this)); + } else { + this.fields.map(function(field) { + key = this.getKey('dd', field.key); + items.push(<dt key={key}>{field.title}</dt>); + key = this.getKey('dt', field.key); + if(field.type == 'string' || field.type == 'link') { + items.push(<dd key={key}><input type="text" + defaultValue={this.props.obj[field.key]} /> + </dd>); + } else if (field.type == 'bool') { + items.push(<dd key={key}><input type="checkbox" + defaultChecked={this.props.obj[field.key]} /></dd>); + } + }.bind(this)); + items.push(<dd key={this.getKey('dd', 'submit')}> + <button className="btn btn-default"> + Submit + </button> + </dd>); + } + return (<dl className="dl-horizontal">{items}</dl>); + }, + render: function() { + return (<div className="panel panel-default"> + {this.getHeader()} + {this.getBody()} + </div> ); }, + onClickEdit: function() { + this.setState({edit_mode: !this.state.edit_mode}); + }, + onClickRemove: function() { + }, +}; + +var Article = React.createClass({ + mixins: [PanelMixin], + getInitialState: function() {return {edit_mode: false};}, + fields: [], + isEditable: function() {return false;}, + isRemovable: function() {return true;}, + getTitle: function() {return this.props.obj.title;}, + getBody: function() { + return (<div className="panel-body" dangerouslySetInnerHTML={ + {__html: this.props.obj.content}} />); + }, }); var Feed = React.createClass({ - propTypes: {feed: React.PropTypes.object.isRequired}, - render: function() { - var icon = null; - if(this.props.feed.icon_url){ - icon = (<img width="16px" src={this.props.feed.icon_url} />); + mixins: [PanelMixin], + getInitialState: function() { + return {edit_mode: false, filters: this.props.obj.filters}; + }, + isEditable: function() {return true;}, + isRemovable: function() {return true;}, + fields: [{'title': 'Feed title', 'type': 'string', 'key': 'title'}, + {'title': 'Description', 'type': 'string', 'key': 'description'}, + {'title': 'Feed link', 'type': 'link', 'key': 'link'}, + {'title': 'Site link', 'type': 'link', 'key': 'site_link'}, + {'title': 'Enabled', 'type': 'bool', 'key': 'enabled'}, + ], + getTitle: function() {return this.props.obj.title;}, + getFilterRow: function(i, filter) { + return (<dd key={'d' + i + '-' + this.props.obj.id} + className="input-group filter-row"> + <span className="input-group-btn"> + <button className="btn btn-default" type="button" + data-index={i} onClick={this.removeFilterRow}> + <Glyphicon glyph='minus' /> + </button> + </span> + <select name="type" className="form-control" + defaultValue={filter.type}> + <option value='simple match'>simple match</option> + <option value='regex'>regex</option> + </select> + <input type="text" className="form-control" + name="pattern" defaultValue={filter.pattern} /> + <select name="action_on" className="form-control" + defaultValue={filter.action_on}> + <option value="match">match</option> + <option value="no match">no match</option> + </select> + <select name="action" className="form-control" + defaultValue={filter.action}> + <option value="mark as read">mark as read</option> + <option value="mark as favorite">mark as favorite</option> + </select> + </dd>); + }, + getBody: function() { + var filter_rows = []; + for(var i in this.state.filters) { + filter_rows.push(this.getFilterRow(i, this.state.filters[i])); } - var header = (<h4> - {icon}<strong>Title:</strong> {this.props.feed.title} - </h4>); - return (<Panel collapsible defaultExpanded header={header}> + return (<div className="panel-body"> <dl className="dl-horizontal"> - <dt>Description</dt> - <dd>{this.props.feed.description}</dd> <dt>Created on</dt> - <dd>{this.props.feed.created_date}</dd> - <dt>Feed adress</dt> - <dd> - <a href={this.props.feed.link}> - {this.props.feed.link} - </a> + <dd><JarrTime stamp={this.props.obj.created_stamp} + text={this.props.obj.created_date} /> + </dd> + <dt>Last fetched</dt> + <dd><JarrTime stamp={this.props.obj.last_stamp} + text={this.props.obj.last_retrieved} /> </dd> - <dt>Site link</dt> + </dl> + {this.getCore()} + <dl className="dl-horizontal"> + <form> + <dt>Filters</dt> <dd> - <a href={this.props.feed.site_link}> - {this.props.feed.site_link} - </a> + <button className="btn btn-default" + type="button" onClick={this.addFilterRow}> + <Glyphicon glyph='plus' /> + </button> + <button className="btn btn-default">Submit + </button> </dd> - <dt>Last fetched</dt> - <dd>{this.props.feed.last_retrieved}</dd> - <dt>Enabled</dt> - <dd>{this.props.feed.enabled}</dd> + {filter_rows} + </form> </dl> - </Panel> + </div> ); }, + addFilterRow: function() { + var filters = this.state.filters; + filters.push({action: null, action_on: null, + type: null, pattern: null}); + this.setState({filters: filters}); + }, + removeFilterRow: function(evnt) { + var filters = this.state.filters; + delete filters[evnt.target.getAttribute('data-index')]; + this.setState({filters: filters}); + }, }); var Category = React.createClass({ - propTypes: {category: React.PropTypes.object.isRequired}, - render: function() { - return (<Panel header={this.props.category.name}> - test - </Panel> - ); + mixins: [PanelMixin], + getInitialState: function() {return {edit_mode: false};}, + isEditable: function() { + if(this.props.obj.id != 0) {return true;} + else {return false;} + }, + isRemovable: function() {return this.isEditable();}, + fields: [{'title': 'Category name', 'type': 'string', 'key': 'name'}], + getTitle: function() {return this.props.obj.name;}, + getBody: function() { + return (<div className="panel-body"> + {this.getCore()} + </div>); }, }); @@ -112,18 +265,21 @@ var RightPanel = React.createClass({ </ol>); } if(this.state.current == 'article') { - var content = <Article article={this.state.article} />; + var cntnt = (<Article type='article' obj={this.state.article} + key={this.state.article.id} />); } else if(this.state.current == 'feed') { - var content = <Feed feed={this.state.feed} />; + var cntnt = (<Feed type='feed' obj={this.state.feed} + key={this.state.feed.id} />); } else if(this.state.current == 'category') { - var content = <Category category={this.state.category} />; + var cntnt = (<Category type='category' obj={this.state.category} + key={this.state.category.id} />); } return (<Col id="right-panel" xsOffset={2} smOffset={2} mdOffset={7} lgOffset={6} xs={10} sm={10} md={5} lg={6}> {breadcrum} - {content} + {cntnt} </Col> ); }, diff --git a/src/web/js/components/time.react.js b/src/web/js/components/time.react.js new file mode 100644 index 00000000..8b4d47d9 --- /dev/null +++ b/src/web/js/components/time.react.js @@ -0,0 +1,15 @@ +var React = require('react'); +var ReactIntl = require('react-intl'); + +var JarrTime = React.createClass({ + mixins: [ReactIntl.IntlMixin], + propTypes: {stamp: React.PropTypes.number.isRequired, + text: React.PropTypes.string.isRequired}, + render: function() { + return (<time dateTime={this.props.text} title={this.props.text}> + {this.formatRelative(this.props.stamp)} + </time>); + }, +}); + +module.exports = JarrTime; diff --git a/src/web/js/stores/MiddlePanelStore.js b/src/web/js/stores/MiddlePanelStore.js index 1a0a4fab..4a5efd00 100644 --- a/src/web/js/stores/MiddlePanelStore.js +++ b/src/web/js/stores/MiddlePanelStore.js @@ -119,9 +119,10 @@ MiddlePanelStore.dispatchToken = JarrDispatcher.register(function(action) { for (var i in MiddlePanelStore._datas.articles) { if(MiddlePanelStore._datas.articles[i].article_id == action.article.id) { MiddlePanelStore._datas.articles[i].read = true; - console.log(MiddlePanelStore._datas.articles[i]); + break; } } + break; default: // pass } diff --git a/src/web/js/stores/RightPanelStore.js b/src/web/js/stores/RightPanelStore.js index 68d3b7e9..85f336e4 100644 --- a/src/web/js/stores/RightPanelStore.js +++ b/src/web/js/stores/RightPanelStore.js @@ -30,17 +30,19 @@ RightPanelStore.dispatchToken = JarrDispatcher.register(function(action) { if(action.filter_id == null) { RightPanelStore._datas.category = null; RightPanelStore._datas.feed = null; + RightPanelStore._datas.current = null; } else if(action.filter_type == 'category_id') { RightPanelStore._datas.category = MenuStore._datas.categories[action.filter_id]; RightPanelStore._datas.feed = null; RightPanelStore._datas.current = 'category'; + RightPanelStore.emitChange(); } else { RightPanelStore._datas.feed = MenuStore._datas.feeds[action.filter_id]; RightPanelStore._datas.category = MenuStore._datas.categories[RightPanelStore._datas.feed.category_id]; RightPanelStore._datas.current = 'feed'; + RightPanelStore.emitChange(); } - RightPanelStore.emitChange(); break; case ActionTypes.LOAD_ARTICLE: RightPanelStore._datas.feed = MenuStore._datas.feeds[action.article.feed_id]; diff --git a/src/web/models/feed.py b/src/web/models/feed.py index a9c49282..c5fcbe4c 100644 --- a/src/web/models/feed.py +++ b/src/web/models/feed.py @@ -73,6 +73,8 @@ class Feed(db.Model): "link": self.link, "site_link": self.site_link, "etag": self.etag, + "enabled": self.enabled, + "filters": self.filters, "icon_url": self.icon_url, "error_count": self.error_count, "created_date": self.created_date, diff --git a/src/web/static/css/one-page-app.css b/src/web/static/css/one-page-app.css index 9f559ee0..d919a4f7 100644 --- a/src/web/static/css/one-page-app.css +++ b/src/web/static/css/one-page-app.css @@ -120,7 +120,6 @@ position: absolute; top: 2px; right: 4px; - display: block; } #right-panel>ol{ margin-top: 10px; @@ -128,3 +127,31 @@ #right-panel .panel-body img { max-width: 100%; } +#right-panel-heading * { + display: inline; +} +#right-panel-heading h4 img { + margin-right: 5px; +} +#right-panel-heading div.btn-group { + float: right; + margin-top: -5px; + margin-right: -8px; +} +.panel-body dd>input { + width: 100%; +} +.filter-row>select.form-control:first-child { + width: 10%; +} +.filter-row>select.form-control { + width: 15%; +} +.filter-row>select.form-control:last-child { + width: 25%; +} +.filter-row>input.form-control { + width: auto; +} +#filter-button-row>*{ +} diff --git a/src/web/views/views.py b/src/web/views/views.py index 49554cb3..f8549e93 100644 --- a/src/web/views/views.py +++ b/src/web/views/views.py @@ -230,6 +230,7 @@ def signup(): return render_template('signup.html', form=form) +from calendar import timegm from flask import jsonify @@ -250,8 +251,12 @@ def get_menu(): categories[cat_id]['feeds'] = [] feeds = {feed.id: feed.dump() for feed in FeedController(g.user.id).read()} for feed_id, feed in feeds.items(): + feed['created_stamp'] = timegm(feed['created_date'].timetuple()) * 1000 + feed['last_stamp'] = timegm(feed['last_retrieved'].timetuple()) * 1000 feed['category_id'] = feed['category_id'] or 0 feed['unread'] = unread.get(feed['id'], 0) + if not feed['filters']: + feed['filters'] = [] if feed.get('icon_url'): feed['icon_url'] = url_for('icon.icon', url=feed['icon_url']) categories[feed['category_id']]['unread'] += feed['unread'] @@ -284,17 +289,13 @@ def _get_filters(in_dict): return filters -import calendar - - def _articles_to_json(articles, fd_hash=None): return jsonify(**{'articles': [{'title': art.title, 'liked': art.like, 'read': art.readed, 'article_id': art.id, 'selected': False, 'feed_id': art.feed_id, 'category_id': art.category_id or 0, 'feed_title': fd_hash[art.feed_id]['title'] if fd_hash else None, 'icon_url': fd_hash[art.feed_id]['icon_url'] if fd_hash else None, - 'date': art.date, - 'timestamp': calendar.timegm(art.date.timetuple()) * 1000} + 'date': art.date, 'timestamp': timegm(art.date.timetuple()) * 1000} for art in articles.limit(1000)]}) |