Skip to content
This repository has been archived by the owner on Mar 29, 2021. It is now read-only.

Commit

Permalink
add support for literal contributor names. closes #215
Browse files Browse the repository at this point in the history
  • Loading branch information
dsifford committed Dec 16, 2017
1 parent 2b41c43 commit 3d977a5
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { observable } from 'mobx';
import * as React from 'react';
import ManualEntryContainer from '../manual-entry-container';

const people = observable<ABT.TypedPerson>([]);
const people = observable<ABT.Contributor>([]);
const manualData = observable.map<string>();
const errorMessage = observable('');
const mocks = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { mount } from 'enzyme';
import { observable } from 'mobx';
import * as React from 'react';
import People from '../people';
import ContributorList from '../';

const defaultPeople: ABT.TypedPerson[] = [{ family: 'Doe', given: 'John', type: 'author' }];
const peopleStore = observable<ABT.TypedPerson>([]);
const defaultPeople: ABT.Contributor[] = [{ family: 'Doe', given: 'John', type: 'author' }];
const peopleStore = observable<ABT.Contributor>([]);

const setup = (
citationType: CSL.ItemType = 'article-journal',
people: ABT.TypedPerson[] = defaultPeople,
citationType: keyof ABT.FieldMappings = 'article-journal',
people: ABT.Contributor[] = defaultPeople,
) => {
peopleStore.replace(people);
const component = mount(<People citationType={citationType} people={peopleStore} />);
const component = mount(<ContributorList citationType={citationType} people={peopleStore} />);
return {
addButton: component.find('.btn-row Button button'),
component,
Expand All @@ -21,44 +21,44 @@ const setup = (
describe('<People />', () => {
it('should render with a single person', () => {
const { component } = setup();
expect(component.find('Person').length).toBe(1);
expect(component.find('Contributor').length).toBe(1);

const person = component.find('Person').first();
const person = component.find('Contributor').first();
expect(person.find('#person-family-0').prop('value')).toBe('Doe');
expect(person.find('#person-given-0').prop('value')).toBe('John');
});
it('should add an empty author when "Add Contributor" is clicked', () => {
const { component, addButton } = setup();
expect(component.find('Person').length).toBe(1);
expect(component.find('Contributor').length).toBe(1);

addButton.simulate('click');
expect(component.find('Person').length).toBe(2);
expect(component.find('Contributor').length).toBe(2);

const newPerson = component.find('Person').at(1);
const newPerson = component.find('Contributor').at(1);
expect(newPerson.find('#person-family-1').prop('value')).toBe('');
expect(newPerson.find('#person-given-1').prop('value')).toBe('');
expect(newPerson.find('select').prop('value')).toBe('author');
});
it('should remove a person when remove button is clicked', () => {
const people: ABT.TypedPerson[] = [
const people: ABT.Contributor[] = [
{ family: 'Doe', given: 'John', type: 'author' },
{ family: 'Smith', given: 'Jane', type: 'author' },
];
const { component } = setup('article-journal', people);
expect(component.find('Person').length).toBe(2);
expect(component.find('Contributor').length).toBe(2);

const removeButton = component.find('Button button').first();
removeButton.simulate('click');
component.update();

expect(component.find('Person').length).toBe(1);
const person = component.find('Person').first();
expect(component.find('Contributor').length).toBe(1);
const person = component.find('Contributor').first();
expect(person.find('#person-family-0').prop('value')).toBe('Smith');
expect(person.find('#person-given-0').prop('value')).toBe('Jane');
});
it('should update fields when data is changed', () => {
const { component } = setup();
const person = component.find('Person').first();
const person = component.find('Contributor').first();
const familyNameInput = person.find('#person-family-0');
const givenNameInput = person.find('#person-given-0');

Expand Down
122 changes: 122 additions & 0 deletions src/js/dialogs/add/contributor-list/contributor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';

import Button from 'components/button';

interface Props {
contributorTypes: ABT.ContributorField[];
index: number;
contributor: ABT.Contributor;
onRemove(e: React.MouseEvent<HTMLButtonElement>): void;
}

@observer
export default class Contributor extends React.Component<Props> {
static readonly labels = top.ABT.i18n.dialogs.add.contributor;

@observable isLiteral: boolean;

constructor(props: Props) {
super(props);
this.isLiteral = props.contributor.literal ? true : false;
}

@action
update = (e: React.FormEvent<HTMLInputElement | HTMLSelectElement>): void => {
const field = e.currentTarget.dataset.field as keyof ABT.Contributor | undefined;
if (!field) {
throw new ReferenceError('"field" property not set on event target');
}
this.props.contributor[field] = e.currentTarget.value;
};

@action
toggleLiteral = (): void => {
this.props.contributor.family = '';
this.props.contributor.given = '';
this.props.contributor.literal = '';
this.isLiteral = !this.isLiteral;
};

render(): JSX.Element {
const { contributorTypes, contributor, index, onRemove } = this.props;
return (
<div>
<select value={contributor.type} onChange={this.update} data-field="type">
{contributorTypes.map(p => (
<option
key={p.type}
aria-selected={contributor.type === p.type}
value={p.type}
children={p.label}
/>
))}
</select>
{this.isLiteral && (
<input
type="text"
placeholder={Contributor.labels.literal}
aria-label={Contributor.labels.literal}
value={contributor.literal}
data-field="literal"
onChange={this.update}
required={true}
/>
)}
{!this.isLiteral && (
<React.Fragment>
<input
type="text"
placeholder={Contributor.labels.surname}
aria-label={Contributor.labels.surname}
value={contributor.family}
data-field="family"
onChange={this.update}
required={true}
/>
<input
type="text"
placeholder={Contributor.labels.given}
aria-label={Contributor.labels.given}
value={contributor.given}
data-field="given"
onChange={this.update}
required={true}
/>
</React.Fragment>
)}
<Button
flat
icon={this.isLiteral ? 'groups' : 'admin-users'}
label={Contributor.labels.toggleLiteral}
onClick={this.toggleLiteral}
/>
<Button
flat
icon="no-alt"
label={Contributor.labels.remove}
onClick={onRemove}
data-index={index}
/>
<style jsx>{`
div {
display: flex;
padding: 0 5px;
align-items: center;
}
input[type='text'] {
flex: auto;
height: 28px;
line-height: 28px;
font-size: 14px;
}
select,
input {
margin: 0 5px;
}
`}</style>
</div>
);
}
}
60 changes: 60 additions & 0 deletions src/js/dialogs/add/contributor-list/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { action, IObservableArray } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';

import Button from 'components/button';
import Contributor from './contributor';

interface Props {
citationType: keyof ABT.FieldMappings;
people: IObservableArray<ABT.Contributor>;
}

@observer
export default class ContributorList extends React.Component<Props> {
static readonly fieldmaps = top.ABT.i18n.fieldmaps;
static readonly labels = top.ABT.i18n.dialogs.add.contributorList;

@action
add = (): void => {
this.props.people.push({ family: '', given: '', literal: '', type: 'author' });
};

@action
remove = (e: React.MouseEvent<HTMLInputElement>): void => {
const index = parseInt(e.currentTarget.dataset.index!, 10);
this.props.people.remove(this.props.people[index]);
};

render(): JSX.Element {
let key = Date.now();
const contributorTypes = ContributorList.fieldmaps[this.props.citationType].people;
return (
<div>
<h2 children={ContributorList.labels.contributors} />
{this.props.people.map((contributor, i) => (
<Contributor
key={key++}
index={i}
contributorTypes={contributorTypes}
contributor={contributor}
onRemove={this.remove}
/>
))}
<div className="btn-row">
<Button flat label={ContributorList.labels.add} onClick={this.add} />
</div>
<style jsx>{`
.btn-row {
display: flex;
justify-content: center;
padding: 5px;
}
h2 {
font-size: 16px !important;
}
`}</style>
</div>
);
}
}
23 changes: 13 additions & 10 deletions src/js/dialogs/add/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AutociteKind } from './autocite';
import Spinner from 'components/spinner';
import Container from 'dialogs/container';
import PubmedDialog from 'dialogs/pubmed';
import { manualPersonObj } from 'utils/constants';
import ButtonRow from './button-row';
import IdentifierInput from './identifier-input';
import ManualEntryContainer from './manual-entry-container';
Expand All @@ -31,7 +32,7 @@ interface Payload {
attachInline: boolean;
identifierList: string;
manualData: ManualData;
people: ABT.TypedPerson[];
people: ABT.Contributor[];
}

@observer
Expand Down Expand Up @@ -60,10 +61,10 @@ export default class AddDialog extends React.Component<DialogProps> {
isLoading = observable(false);

/** Controls the value of all fields in `ManualEntryContainer` */
manualData = observable.map(new Map([['type', 'webpage']]));
manualData = observable.map<string>(new Map([['type', 'webpage']]));

/** Controls the value of the `People` fields in `ManualEntryContainer` */
people = observable<ABT.TypedPerson>([{ family: '', given: '', type: 'author' }]);
people = observable<ABT.Contributor>([{ ...manualPersonObj }]);

@computed
get payload(): Payload {
Expand Down Expand Up @@ -109,10 +110,10 @@ export default class AddDialog extends React.Component<DialogProps> {
title: meta.content_title,
});
this.people.replace(
meta.authors.map(a => ({
family: a.lastname || '',
given: a.firstname || '',
type: 'author' as CSL.PersonType,
meta.authors.map(author => ({
...manualPersonObj,
family: author.lastname || '',
given: author.firstname || '',
})),
);
break;
Expand All @@ -130,7 +131,9 @@ export default class AddDialog extends React.Component<DialogProps> {
publisher: meta.publisher,
[titleKey]: meta.title,
});
this.people.replace(meta.authors);
this.people.replace(
meta.authors.map(author => ({ ...manualPersonObj, ...author })),
);
}
};

Expand All @@ -143,7 +146,7 @@ export default class AddDialog extends React.Component<DialogProps> {
changeType = (citationType: CSL.ItemType): void => {
this.manualData.clear();
this.manualData.set('type', citationType);
this.people.replace([{ family: '', given: '', type: 'author' }]);
this.people.replace([{ ...manualPersonObj }]);
};

@action
Expand All @@ -165,7 +168,7 @@ export default class AddDialog extends React.Component<DialogProps> {
@action
toggleAddManual = (): void => {
this.addManually.set(!this.addManually.get());
this.people.replace([{ family: '', given: '', type: 'author' }]);
this.people.replace([{ ...manualPersonObj }]);
this.changeType('webpage');
};

Expand Down
10 changes: 6 additions & 4 deletions src/js/dialogs/add/manual-entry-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import * as React from 'react';

import Callout from 'components/callout';
import AutoCite, { AutociteKind } from './autocite';
import ContributorList from './contributor-list/';
import MetaFields from './meta-fields';
import People from './people';

interface ManualEntryProps {
/** Error message to display in callout. If none, this should be an empty string */
errorMessage: IObservableValue<string>;
/** Observable map of `CSL.Data` for manual entry fields */
manualData: ObservableMap<string>;
people: IObservableArray<ABT.TypedPerson>;
people: IObservableArray<ABT.Contributor>;
/** "Getter" callback for `AutoCite` component */
onAutoCite(kind: AutociteKind, query: string): void;
/** Callback with new `CSL.ItemType` to call when type is changed */
Expand Down Expand Up @@ -105,9 +105,11 @@ export default class ManualEntryContainer extends React.Component<ManualEntryPro
onDismiss={this.dismissError}
/>
{this.props.manualData.get('type') !== 'article' && (
<People
<ContributorList
people={this.props.people}
citationType={this.props.manualData.get('type') as CSL.ItemType}
citationType={
this.props.manualData.get('type') as keyof ABT.FieldMappings
}
/>
)}
<MetaFields meta={this.props.manualData} />
Expand Down
Loading

0 comments on commit 3d977a5

Please sign in to comment.