Skip to content

Commit

Permalink
Text spanner renderer and Article Viewing and paging in Researcher UI (
Browse files Browse the repository at this point in the history
…#170)

* Stub in ArticleView container.

* Add basic layout styling to ArticleView.

* Stub in Spanner and SpannerState.

* Stub in props for Spanner. Rename SpannerState to EditorState.

* Code for creating and tracking DisplayState.

* Add prop-types package that replaces deprecated React.PropTypes.

* Framing in convertToSpanner, EditorState, ContentState, and Block.

* Implement block rendering. By default use line breaks to create blocks.

* Refactor Blocks to be part of DisplayState.

* Change block function to return props to merge instead of just style.

* Add topic name and order to Highlight Group serializer.

Assign order numbers when creating topics for unmatched TUA types.

* Begining of revisions to enable Spanner to render nested blocks.

* Add classes for modeling layers of annotations to Spanner.

Add articleView code to load highlights into EditorState.

* Add algorithm to generate character spans from potentially overlapping layers.

* Add span rendering to TextSpanner.

* Create SpanSegment and cache per block.

Cache CharacterClasses once per EditorState.

* DisplayState provides ordered layers to mergeStyleFn.

Improve naming and cache management of EditorState charAnnotations.

* Cleanups.

* Add component to display article metadata.

* Now using django_filter on endpoints to query for multiple article_numbers at a time.

URLs moved to /researcher/articles/ and /api/articles/

Formerly article endpoints retrieved a single article dataset based on the Django DB id.

* Add slider control to ArticleView.

* Add pager controls to ArticleView.

* Improve Pager control.

Minor tweaks to ArticleSlider, ArticleView, ArticleMetaData components.

* Add validation for annotation and block offsets in EditorState.

* ArticleView passes state change function to Pager so Pager can be a pure component.

* Use a good key for span.

* Add 'View Article Highlights' link to Researcher menu, and as action to Article admin.

* Create ConsistentColors class so ArticleView topic colors are consistent between articles.

Use top and bottom border colors for overlapping annotations.

Generalize params for displayLinesAsBlocks and move to TextSpanner/utils.

* Sort layers by topic name and case number in Article View.

* Add wrapSpanFn prop to Spanner.

Provide default no-op functions for Spanner function props.

* Allow LayerState to be labeled with any object caller wants to use as label.

This is to support highlights as topics or highlights as answers.

Move domain specific TopicLabel class out of TextSpanner tree.
  • Loading branch information
normangilmore authored Nov 9, 2017
1 parent 2236132 commit c6a2d43
Show file tree
Hide file tree
Showing 37 changed files with 1,114 additions and 51 deletions.
12 changes: 9 additions & 3 deletions app/actions/articleView.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
export function initArticleReview() {
export function initArticleView() {
return { type: 'FETCH_ARTICLE_REVIEW' };
}

export function storeArticleReview(normalizedData) {
export function storeArticleView(normalizedData) {
return { type: 'FETCH_ARTICLE_REVIEW_SUCCESS',
normalizedData
};
}

export function errorArticleReview(error) {
export function showArticleView(article_id) {
return { type: 'DISPLAY_ARTICLE_VIEW',
article_id
};
}

export function errorArticleView(error) {
return { type: 'FETCH_ARTICLE_REVIEW_FAIL',
error
};
Expand Down
25 changes: 18 additions & 7 deletions app/articleView.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@ import React from 'react';
import { render } from 'react-dom';
import { bindActionCreators } from 'redux';

let debug = require('debug')('thresher:articleView');

import App, { store } from 'containers/App';
import { ArticleView } from 'containers/ArticleView';
import * as articleViewActions from 'actions/articleView';
import fetchArticleView from 'django/articleView';

let reduxActions = bindActionCreators(articleViewActions, store.dispatch);

render(
<App>
<div>Still working on this.</div>
</App>,
document.getElementById('react-root')
);
try {
render(
<App>
<ArticleView>
<div>Loading...</div>
</ArticleView>
</App>,
document.getElementById('react-root')
);
} catch (error) {
reduxActions.errorArticleView(error);
};

// This function is called with the URL of the API endpoint
// by the Django page hosting this script.
export function loadArticle(articleFetchURL) {
console.log(`Fetching: ${articleFetchURL}`);
debug(`Fetching: ${articleFetchURL}`);
fetchArticleView(reduxActions, articleFetchURL);
};
78 changes: 78 additions & 0 deletions app/components/ArticleMetaData/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';

function appendIfExists(metadata, fieldOrder) {
let sequence = [];
for (let field of fieldOrder) {
if (field.name in metadata
&& metadata[field.name] !== null
&& metadata[field.name] !== undefined) {
if (field.label !== '') {
sequence.push(field.label + ": " + metadata[field.name]);
} else {;
sequence.push(metadata[field.name]);
};
};
};
return sequence.join(', ');;
}

export class ArticleMetaData extends React.Component {
constructor(props) {
super(props);
}

static propTypes = {
metadata: PropTypes.object.isRequired,
}

render() {
let metadata = this.props.metadata;

let fieldOrder = [
{name: 'article_number', label: 'Article'},
{name: 'periodical', label: ''},
{name: 'periodical_code', label: 'Periodical code'},
{name: 'city', label: ''},
{name: 'state', label: ''},
{name: 'date_published', label: 'Published'},
{name: 'version', label: 'version'},
];
let elements = [];
let citation = appendIfExists(metadata, fieldOrder);
if (citation !== '') {
elements.push(
<div key='1' className="article-citation">
{citation}
</div>
);
};

// Only Deciding Force articles have annotators in their metadata.
if ('annotators' in metadata) {
let annotators = metadata['annotators'].join(' ');
if (annotators !== '') {
elements.push(
<div key='2' className="article-annotator">
Annotators: {annotators}
</div>
);
};
};

fieldOrder = [{name: 'filename', label: 'Filename'}];
let filename = appendIfExists(metadata, fieldOrder);
if (filename !== '') {
elements.push(
<div key='3' className="article-filename">
{filename}
</div>
);
};
return (
<div>
{elements}
</div>
);
}
}
26 changes: 26 additions & 0 deletions app/components/ArticleSlider/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';

export class ArticleSlider extends React.Component {
constructor(props) {
super(props);
}

static propTypes = {
article_index: PropTypes.number.isRequired,
article_ids: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
}

render() {
return (
<input type="range"
value={this.props.article_index}
min={0}
max={this.props.article_ids.length - 1}
step={1}
onChange={this.props.onChange}
/>
);
}
}
47 changes: 47 additions & 0 deletions app/components/Pager/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';

export class Pager extends React.Component {
constructor(props) {
super(props);
}

static propTypes = {
result: PropTypes.object.isRequired,
fetchArticles: PropTypes.func.isRequired,
}

render() {
let elem = [];
let result = this.props.result;
if (result.previous !== null) {
elem.push(
<a key="prev"
onClick={ (e) => {
this.props.fetchArticles(result.previous);
e.preventDefault();
}}
style={{'cursor': 'pointer'}}
className="previous-group-button">
Previous Group
</a>);
};
if (result.next !== null) {
elem.push(
<a key="next"
onClick={ (e) => {
this.props.fetchArticles(result.next);
e.preventDefault();
}}
style={{'cursor': 'pointer', 'float': 'right'}}
className="next-group-button">
Next Group
</a>);
};
return (
<div className="prev-next-pager clearfix">
{elem}
</div>
);
}
}
75 changes: 75 additions & 0 deletions app/components/TextSpanner/components/Spanner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';

import { EditorState } from '../model/EditorState';
import { DisplayState } from '../model/DisplayState';

const debug = require('debug')('thresher:TextSpanner');

export class Spanner extends React.Component {
constructor(props) {
super(props);
}

static propTypes = {
editorState: PropTypes.instanceOf(EditorState).isRequired,
displayState: PropTypes.instanceOf(DisplayState).isRequired,
blockPropsFn: PropTypes.func,
mergeStyleFn: PropTypes.func,
wrapSpanFn: PropTypes.func,
}

static defaultProps = {
blockPropsFn: ((block, sequence_number) => {}),
mergeStyleFn: ((orderedLayers) => {}),
wrapSpanFn: ((span) => span)
};

render() {
const editorState = this.props.editorState;
const displayState = this.props.displayState;
const blockPropsFn = this.props.blockPropsFn;
const mergeStyleFn = this.props.mergeStyleFn;
const wrapSpanFn = this.props.wrapSpanFn;
const text = editorState.getText();

function renderSpans(block) {
let spans = editorState.getSpans(block);
return (spans.map( (span, i) => {
let orderedLayers = displayState.getOrderedLayersFor(span.spanAnnotations);
let mergedStyle = mergeStyleFn(orderedLayers);
let titleList = orderedLayers.map( (ola) => ola.annotation.topicName );
let title = titleList.join(', ');
return (wrapSpanFn(
<span key={span.key}
data-offset-start={span.start}
data-offset-end={span.end}
style={mergedStyle}
title={title}>
{text.substring(span.start, span.end)}
</span>
));
}));
};

function renderBlock(block, i) {
const mergeProps = blockPropsFn(block, i);
return React.cloneElement(
<div key={block.key}
data-offset-start={block.start}
data-offset-end={block.end}>
{renderSpans(block)}
</div>,
mergeProps
);
};

return (
<div>
{displayState.getBlockList().map( (block, i) => {
return renderBlock(block, i);
})}
</div>
);
}
}
3 changes: 3 additions & 0 deletions app/components/TextSpanner/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {Spanner} from './components/Spanner';
export {EditorState} from './model/EditorState';
export {displayLinesAsBlocks} from './utils';
25 changes: 25 additions & 0 deletions app/components/TextSpanner/model/Annotation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Record } from 'immutable';

import generateRandomKey from '../utils/generateRandomKey';

const debug = require('debug')('thresher:TextSpanner');

const defaultAnnotationRecord = {
key: '',
topicName: '',
topicOrder: 0,
caseNumber: 0,
start: 0,
end: 0,
contributor: {},
extra: {},
}

const AnnotationRecord = Record(defaultAnnotationRecord);

export class Annotation extends AnnotationRecord {
constructor(annotation) {
annotation['key'] = generateRandomKey();
super(annotation);
}
}
23 changes: 23 additions & 0 deletions app/components/TextSpanner/model/Block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Record } from 'immutable';

import generateRandomKey from '../utils/generateRandomKey';

const debug = require('debug')('thresher:TextSpanner');

const defaultBlockRecord = {
key: '',
blockType: 'unstyled',
start: 0,
end: 0,
depth: 0,
options: {}
}

const BlockRecord = Record(defaultBlockRecord);

export class Block extends BlockRecord {
constructor(block) {
block['key'] = generateRandomKey();
super(block);
}
}
22 changes: 22 additions & 0 deletions app/components/TextSpanner/model/ContentState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Record } from 'immutable';

import generateRandomKey from '../utils/generateRandomKey';

const debug = require('debug')('thresher:TextSpanner');

export class ContentState {
constructor() {
this._text = "";
this.setText = this._setText.bind(this);
this.getText = this._getText.bind(this);
}

_setText(text) {
this._text = text;
}

_getText() {
return this._text;
}

}
Loading

0 comments on commit c6a2d43

Please sign in to comment.