diff --git a/src/client/entity-editor/alias-editor/actions.ts b/src/client/entity-editor/alias-section/actions.ts similarity index 84% rename from src/client/entity-editor/alias-editor/actions.ts rename to src/client/entity-editor/alias-section/actions.ts index d5082d9c0e..9396def283 100644 --- a/src/client/entity-editor/alias-editor/actions.ts +++ b/src/client/entity-editor/alias-section/actions.ts @@ -18,7 +18,7 @@ export const UPDATE_ALIAS_NAME = 'UPDATE_ALIAS_NAME'; export const UPDATE_ALIAS_SORT_NAME = 'UPDATE_ALIAS_SORT_NAME'; -export const ADD_ALIAS_ROW = 'ADD_ALIAS_ROW'; +export const ADD_ALIAS = 'ADD_ALIAS'; export const UPDATE_ALIAS_LANGUAGE = 'UPDATE_ALIAS_LANGUAGE'; export const UPDATE_ALIAS_PRIMARY = 'UPDATE_ALIAS_PRIMARY'; export const REMOVE_ALIAS_ROW = 'REMOVE_ALIAS_ROW'; @@ -33,6 +33,26 @@ export type Action = { } }; +let nextAliasRowId = 0; + +export function addNewAlias( + nameValue: string, sortNameValue: string, LanguageValue: number | null | undefined, + primary: boolean +): Action { + return { + payload: { + data: { + language: LanguageValue, + name: nameValue, + primary, + sortName: sortNameValue + }, + rowId: `n${nextAliasRowId++}` + }, + type: ADD_ALIAS + }; +} + /** * Produces an action indicating that the name for a particular alias within * the editor should be updated with the provided value. The action is @@ -113,26 +133,6 @@ export function updateAliasPrimary(rowId: number, value: boolean): Action { }; } -let nextAliasRowId = 0; - -/** - * Produces an action indicating that a row for a new alias should be added - * to the alias editor. The row is assigned an ID based on an incrementing - * variable existing on the client. - * - * @returns {Action} The resulting ADD_ALIAS_ROW action. - */ -export function addAliasRow(): Action { - /* - * Prepend 'n' here to indicate new alias, and avoid conflicts with IDs of - * existing aliases. - */ - return { - payload: `n${nextAliasRowId++}`, - type: ADD_ALIAS_ROW - }; -} - /** * Produces an action indicating that the row with the provided ID should be * removed from the alias editor. @@ -147,20 +147,6 @@ export function removeAliasRow(rowId: number): Action { }; } -/** - * Produces an action indicating that the alias editor should be hidden from - * view. - * - * @see showAliasEditor - * - * @returns {Action} The resulting HIDE_ALIAS_EDITOR action. - */ -export function hideAliasEditor(): Action { - return { - type: HIDE_ALIAS_EDITOR - }; -} - /** * Produces an action indicating that the empty rows should be deleted. * diff --git a/src/client/entity-editor/alias-editor/alias-editor-merge.js b/src/client/entity-editor/alias-section/alias-editor-merge.js similarity index 98% rename from src/client/entity-editor/alias-editor/alias-editor-merge.js rename to src/client/entity-editor/alias-section/alias-editor-merge.js index 81d4ab7ad4..7873f5a7e5 100644 --- a/src/client/entity-editor/alias-editor/alias-editor-merge.js +++ b/src/client/entity-editor/alias-section/alias-editor-merge.js @@ -71,7 +71,7 @@ AliasEditorMerge.propTypes = { function mapStateToProps(rootState) { const nameSection = rootState.get('nameSection'); - const aliases = rootState.get('aliasEditor'); + const aliases = rootState.get('aliasSection'); /** Dynamically filter out aliases that are different * from the attributes selected in the nameSection diff --git a/src/client/entity-editor/alias-section/alias-editor.tsx b/src/client/entity-editor/alias-section/alias-editor.tsx new file mode 100644 index 0000000000..6a23f35d2d --- /dev/null +++ b/src/client/entity-editor/alias-section/alias-editor.tsx @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2016 Ben Ockmore + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {Button, Col, Form, Modal, Row} from 'react-bootstrap'; +import { + validateAliasLanguage, validateAliasName, validateAliasSortName +} from '../validators/common'; +import LanguageField from '../common/language-field'; +import NameField from '../common/name-field'; +import PropTypes from 'prop-types'; +import React from 'react'; +import SortNameField from '../common/sort-name-field'; +import {connect} from 'react-redux'; +import {isAliasEmpty} from '../helpers'; + + +const onClose = (setVisibility, removeEmptyAliases) => { + setVisibility(false); + removeEmptyAliases(); +}; + +function AliasEditor({ + show, + setVisibility, + nameValue, + sortNameValue, + aliasLanguage, + languageOptions, + languageValue, + primaryCheck, + onLanguageChange, + onNameChange, + onSortNameChange, + onPrimaryClick, + removeEmptyAliases +}) { + const handleModalCLose = React.useCallback(() => { + onClose(setVisibility, removeEmptyAliases); + }, []); + return ( + + + + Identifier Editor + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + +
+ ); +} +AliasEditor.displayName = 'AliasEditor'; +AliasEditor.propTypes = { + aliasLanguage: PropTypes.number.isRequired, + languageOptions: PropTypes.array.isRequired, + languageValue: PropTypes.array.isRequired, + nameValue: PropTypes.string.isRequired, + onLanguageChange: PropTypes.func.isRequired, + onNameChange: PropTypes.func.isRequired, + onPrimaryClick: PropTypes.func.isRequired, + onSortNameChange: PropTypes.func.isRequired, + primaryCheck: PropTypes.bool.isRequired, + removeEmptyAliases: PropTypes.func.isRequired, + setVisibility: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + sortNameValue: PropTypes.string.isRequired +}; + + +export default connect(null, null)(AliasEditor); diff --git a/src/client/entity-editor/alias-section/alias-fields.tsx b/src/client/entity-editor/alias-section/alias-fields.tsx new file mode 100644 index 0000000000..610f602aac --- /dev/null +++ b/src/client/entity-editor/alias-section/alias-fields.tsx @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 Ben Ockmore + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {Button, Col, Container, Form, Row} from 'react-bootstrap'; +import { + validateAliasLanguage, validateAliasName, validateAliasSortName +} from '../validators/common'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import LanguageField from '../common/language-field'; +import NameField from '../common/name-field'; +import PropTypes from 'prop-types'; +import React from 'react'; +import SortNameField from '../common/sort-name-field'; +import {addNewAlias} from './actions'; +import {connect} from 'react-redux'; +import {faPlus} from '@fortawesome/free-solid-svg-icons'; +import {isAliasEmpty} from '../helpers'; + + +type LanguageObj = { + frequency: number, + label: string, + value: number +}; + +function AliasFields({ + languageOptions, + onAddNewAlias +}) { + const [nameValue, setNameValue] = React.useState(''); + const [sortNameValue, setSortNameValue] = React.useState(''); + const [languageValue, setLanguageValue] = React.useState(null); + const [primaryCheck, setPrimaryCheckbox] = React.useState(false); + + const handleNameChange = React.useCallback((event: React.ChangeEvent) => { + setNameValue(event.target.value); + }, []); + const handleSortNameChange = React.useCallback((event: React.ChangeEvent) => { + setSortNameValue(event.target.value); + }, []); + const handlePrimaryCheckboxChange = React.useCallback((event: React.ChangeEvent) => { + setPrimaryCheckbox(event.target.checked); + }, []); + + const handleAddAliasButtonClick = React.useCallback(() => { + onAddNewAlias(nameValue, sortNameValue, languageValue?.value, primaryCheck); + }, [nameValue, sortNameValue, languageValue?.value, primaryCheck]); + + return ( + + + + + + + + + + el.value === languageValue).label} + onChange={setLanguageValue} + /> + + +
+
+ +
+
+ +
+
+ +
+
+ ); +} +AliasFields.displayName = 'AliasEditor.AliasRow'; +AliasFields.propTypes = { + languageOptions: PropTypes.array.isRequired, + onAddNewAlias: PropTypes.func.isRequired +}; + +function mapDispatchToProps(dispatch) { + return { + onAddNewAlias: (nameValue, sortNameValue, languageValue, primary) => + dispatch(addNewAlias(nameValue, sortNameValue, languageValue, primary)) + }; +} + +export default connect(null, mapDispatchToProps)(AliasFields); diff --git a/src/client/entity-editor/alias-editor/alias-row-merge.js b/src/client/entity-editor/alias-section/alias-row-merge.js similarity index 98% rename from src/client/entity-editor/alias-editor/alias-row-merge.js rename to src/client/entity-editor/alias-section/alias-row-merge.js index 1961630e7c..585b466326 100644 --- a/src/client/entity-editor/alias-editor/alias-row-merge.js +++ b/src/client/entity-editor/alias-section/alias-row-merge.js @@ -75,7 +75,7 @@ function mapDispatchToProps(dispatch, {index}) { } function mapStateToProps(rootState, {index}) { - const state = rootState.get('aliasEditor'); + const state = rootState.get('aliasSection'); return { languageValue: state.getIn([index, 'language']), nameValue: state.getIn([index, 'name']), diff --git a/src/client/entity-editor/alias-editor/alias-row.js b/src/client/entity-editor/alias-section/alias-row.js similarity index 61% rename from src/client/entity-editor/alias-editor/alias-row.js rename to src/client/entity-editor/alias-section/alias-row.js index e744f0c277..6cdf57edb3 100644 --- a/src/client/entity-editor/alias-editor/alias-row.js +++ b/src/client/entity-editor/alias-section/alias-row.js @@ -16,36 +16,33 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import {Button, Col, Form, Row} from 'react-bootstrap'; +import {Col, Row} from 'react-bootstrap'; import { debouncedUpdateAliasName, debouncedUpdateAliasSortName, removeAliasRow, + removeEmptyAliases, updateAliasLanguage, updateAliasPrimary } from './actions'; -import { - validateAliasLanguage, validateAliasName, validateAliasSortName -} from '../validators/common'; +import {faPencilAlt, faTrash} from '@fortawesome/free-solid-svg-icons'; +import AliasEditor from './alias-editor'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import LanguageField from '../common/language-field'; -import NameField from '../common/name-field'; import PropTypes from 'prop-types'; import React from 'react'; -import SortNameField from '../common/sort-name-field'; import {connect} from 'react-redux'; -import {faTimes} from '@fortawesome/free-solid-svg-icons'; -import {isAliasEmpty} from '../helpers'; /** * Container component. The AliasRow component renders a single Row containing - * several input fields, allowing the user to set the name, sort name, language - * and primary flag for an alias in the AliasEditor. A button is also included - * to remove the alias from the editor. + * alias data of the entity, which displays name, sort name, language + * and primary flag for an alias in the AliasSection. two icon button is also included + * to remove and edit the alias in the aliasEditor. * * @param {Object} props - The properties passed to the component. - * @param {number} props.languageValue - The ID of the language currently - * selected. * @param {Array} props.languageOptions - The list of possible languages for an * alias. + * * @param {Object} props.alias - the alias obejct containing information about the alias + * passed to the component + * @param {number} props.languageValue - The ID of the language currently + * selected. * @param {string} props.nameValue - The name currently set for this alias. * @param {string} props.sortNameValue - The sort name currently set for this * alias. @@ -63,95 +60,89 @@ import {isAliasEmpty} from '../helpers'; * the primary checkbox is clicked. * @returns {ReactElement} React element containing the rendered AliasRow. */ -const AliasRow = ({ - languageOptions, - languageValue, + +function AliasRow({ + aliasLanguage, nameValue, sortNameValue, primaryChecked, + languageOptions, onLanguageChange, onNameChange, onSortNameChange, onRemoveButtonClick, + onRemoveEmptyAliases, onPrimaryClick -}) => ( -
- - - - - - - - - el.value === aliasLanguage); + const [showEditor, setEditorVisibility] = React.useState(false); + + const handleEditButtonClick = React.useCallback(() => { + setEditorVisibility(true); + }, []); + + return ( +
+ + + + {primaryChecked ? + {nameValue} ({sortNameValue}) - {languageValue[0]?.label} : + {nameValue} ({sortNameValue}) - {languageValue[0]?.label} } - error={!validateAliasLanguage(languageValue)} - instanceId="language" - options={languageOptions} - value={languageOptions.filter((el) => el.value === languageValue)} - onChange={onLanguageChange} - /> - - - - - - - - - - -
-
-); + + + + + + + +
+
+ ); +} AliasRow.displayName = 'AliasEditor.AliasRow'; AliasRow.propTypes = { + aliasLanguage: PropTypes.number.isRequired, languageOptions: PropTypes.array.isRequired, - languageValue: PropTypes.number, nameValue: PropTypes.string.isRequired, onLanguageChange: PropTypes.func.isRequired, onNameChange: PropTypes.func.isRequired, onPrimaryClick: PropTypes.func.isRequired, onRemoveButtonClick: PropTypes.func.isRequired, + onRemoveEmptyAliases: PropTypes.func.isRequired, onSortNameChange: PropTypes.func.isRequired, primaryChecked: PropTypes.bool.isRequired, sortNameValue: PropTypes.string.isRequired }; -AliasRow.defaultProps = { - languageValue: null -}; + +function mapStateToProps(rootState, {index}) { + const state = rootState.get('aliasSection'); + return { + aliasLanguage: state.getIn([index, 'language']), + nameValue: state.getIn([index, 'name']), + primaryChecked: state.getIn([index, 'primary']), + sortNameValue: state.getIn([index, 'sortName']) + }; +} + function mapDispatchToProps(dispatch, {index}) { return { @@ -163,20 +154,11 @@ function mapDispatchToProps(dispatch, {index}) { dispatch(updateAliasPrimary(index, event.target.checked)), onRemoveButtonClick: () => dispatch(removeAliasRow(index)), + onRemoveEmptyAliases: () => + dispatch(removeEmptyAliases()), onSortNameChange: (event) => dispatch(debouncedUpdateAliasSortName(index, event.target.value)) }; } -function mapStateToProps(rootState, {index}) { - const state = rootState.get('aliasEditor'); - return { - languageValue: state.getIn([index, 'language']), - nameValue: state.getIn([index, 'name']), - primaryChecked: state.getIn([index, 'primary']), - sortNameValue: state.getIn([index, 'sortName']) - }; -} - - export default connect(mapStateToProps, mapDispatchToProps)(AliasRow); diff --git a/src/client/entity-editor/alias-editor/alias-modal-body.tsx b/src/client/entity-editor/alias-section/alias-section-body.tsx similarity index 56% rename from src/client/entity-editor/alias-editor/alias-modal-body.tsx rename to src/client/entity-editor/alias-section/alias-section-body.tsx index 5b2e31c490..684552d16d 100644 --- a/src/client/entity-editor/alias-editor/alias-modal-body.tsx +++ b/src/client/entity-editor/alias-section/alias-section-body.tsx @@ -1,27 +1,23 @@ -import {Button, Col, Row} from 'react-bootstrap'; + +import AliasFields from './alias-fields'; import AliasRow from './alias-row'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import Immutable from 'immutable'; import React from 'react'; -import {addAliasRow} from './actions'; +import {Row} from 'react-bootstrap'; import classNames from 'classnames'; import {connect} from 'react-redux'; -import {faPlus} from '@fortawesome/free-solid-svg-icons'; type AliasModalBodyStateProps = { aliases: Immutable.List; }; -type AliasModalBodyDispatchProps = { - onAddAlias: () => void; -}; type AliasModalBodyOwnProps = { languageOptions?:any[], }; -type AliasModalBodyProps = AliasModalBodyStateProps & AliasModalBodyDispatchProps & AliasModalBodyOwnProps; +type AliasModalBodyProps = AliasModalBodyStateProps & AliasModalBodyOwnProps; -export const AliasModalBody = ({aliases, onAddAlias, languageOptions}:AliasModalBodyProps) => { +export const AliasModalBody = ({aliases, languageOptions}:AliasModalBodyProps) => { const noAliasesTextClass = classNames('text-center', {'d-none': aliases.size}); const languageOptionsForDisplay = languageOptions.map((language) => ({ @@ -47,12 +43,12 @@ export const AliasModalBody = ({aliases, onAddAlias, languageOptions}:AliasModal } - - - + ); }; @@ -61,16 +57,10 @@ AliasModalBody.defaultProps = { languageOptions: [] }; -function mapDispatchToProps(dispatch) { - return { - onAddAlias: () => dispatch(addAliasRow()) - }; -} - function mapStateToProps(rootState) { return { - aliases: rootState.get('aliasEditor') + aliases: rootState.get('aliasSection') }; } -export default connect(mapStateToProps, mapDispatchToProps)(AliasModalBody); +export default connect(mapStateToProps, null)(AliasModalBody); diff --git a/src/client/entity-editor/alias-editor/alias-editor.js b/src/client/entity-editor/alias-section/alias-section.js similarity index 54% rename from src/client/entity-editor/alias-editor/alias-editor.js rename to src/client/entity-editor/alias-section/alias-section.js index 3edf74c36f..c86a974657 100644 --- a/src/client/entity-editor/alias-editor/alias-editor.js +++ b/src/client/entity-editor/alias-section/alias-section.js @@ -15,37 +15,25 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ - -import {Button, Modal, OverlayTrigger, Tooltip} from 'react-bootstrap'; -import {hideAliasEditor, removeEmptyAliases} from './actions'; -import AliasModalBody from './alias-modal-body'; +import {OverlayTrigger, Tooltip} from 'react-bootstrap'; +import AliasModalBody from './alias-section-body'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; - /** - * Container component. The AliasEditor component contains a number of AliasRow - * elements, and renders these inside a modal, which appears when the show - * property of the component is set. + * Container component. The AliasSection component contains alias section + * body, and renders it in alias section inside entity editor page. * * @param {Object} props - The properties passed to the component. - * @param {Array} props.aliases - The list of aliases to be rendered in the - * editor. * @param {Array} props.languageOptions - The list of possible languages for an * alias. - * @param {Function} props.onClose - A function to be called when the button to - * close the editor is clicked. - * @param {boolean} props.show - Whether or not the editor modal should be - * visible. * @returns {ReactElement} React element containing the rendered AliasEditor. */ -const AliasEditor = ({ - languageOptions, - onClose, - show +const AliasSection = ({ + languageOptions }) => { const helpText = `Variant names for an entity such as alternate spelling, different script, stylistic representation, acronyms, etc. Refer to the help page for more details and examples.`; @@ -63,40 +51,24 @@ const AliasEditor = ({ ); return ( - - - - Alias Editor {helpIconElement} - - - - +
+
+
+

Add new alias

+
+ {helpIconElement} +
+
+
+
- - - - - - - ); -}; -AliasEditor.displayName = 'AliasEditor'; -AliasEditor.propTypes = { - languageOptions: PropTypes.array.isRequired, - onClose: PropTypes.func.isRequired, - show: PropTypes.bool +
+
+ ); }; -AliasEditor.defaultProps = { - show: false +AliasSection.displayName = 'AliasSection'; +AliasSection.propTypes = { + languageOptions: PropTypes.array.isRequired }; -function mapDispatchToProps(dispatch) { - return { - onClose: () => { - dispatch(hideAliasEditor()); - dispatch(removeEmptyAliases()); - } - }; -} - -export default connect(null, mapDispatchToProps)(AliasEditor); +export default connect(null, null)(AliasSection); diff --git a/src/client/entity-editor/alias-editor/reducer.js b/src/client/entity-editor/alias-section/reducer.js similarity index 86% rename from src/client/entity-editor/alias-editor/reducer.js rename to src/client/entity-editor/alias-section/reducer.js index dc0d18eb71..a5fe279281 100644 --- a/src/client/entity-editor/alias-editor/reducer.js +++ b/src/client/entity-editor/alias-section/reducer.js @@ -17,27 +17,23 @@ */ import { - ADD_ALIAS_ROW, REMOVE_ALIAS_ROW, REMOVE_EMPTY_ALIASES, UPDATE_ALIAS_LANGUAGE, + ADD_ALIAS, + REMOVE_ALIAS_ROW, REMOVE_EMPTY_ALIASES, UPDATE_ALIAS_LANGUAGE, UPDATE_ALIAS_NAME, UPDATE_ALIAS_PRIMARY, UPDATE_ALIAS_SORT_NAME } from './actions'; import Immutable from 'immutable'; -const EMPTY_ALIAS = Immutable.Map({ - language: null, - name: '', - primary: false, - sortName: '' -}); - function reducer( state = Immutable.OrderedMap(), action ) { const {payload, type} = action; switch (type) { - case ADD_ALIAS_ROW: - return state.set(payload, EMPTY_ALIAS); + case ADD_ALIAS: { + const ALIAS_DATA = Immutable.Map(payload.data); + return state.set(payload.rowId, ALIAS_DATA); + } case UPDATE_ALIAS_NAME: return state.setIn([payload.rowId, 'name'], payload.value); case UPDATE_ALIAS_SORT_NAME: diff --git a/src/client/entity-editor/author-credit-editor/author-credit-row.tsx b/src/client/entity-editor/author-credit-editor/author-credit-row.tsx index 7a97873b2f..428949d096 100644 --- a/src/client/entity-editor/author-credit-editor/author-credit-row.tsx +++ b/src/client/entity-editor/author-credit-editor/author-credit-row.tsx @@ -60,7 +60,7 @@ type Props = StateProps & DispatchProps & OwnProps; /** * Container component. The IdentifierRow component renders a single Row * containing several input fields, allowing the user to set the value and type - * for an identifier in the IdentifierEditor. A button is also included to + * for an identifier in the IdentifierSection. A button is also included to * remove the identifier from the editor. * * @param {Object} props - The properties passed to the component. diff --git a/src/client/entity-editor/button-bar/button-bar.js b/src/client/entity-editor/button-bar/button-bar.js index 3539e8a247..b9bdc867c4 100644 --- a/src/client/entity-editor/button-bar/button-bar.js +++ b/src/client/entity-editor/button-bar/button-bar.js @@ -27,21 +27,20 @@ import AliasButton from './alias-button'; import IdentifierButton from './identifier-button'; import PropTypes from 'prop-types'; import React from 'react'; -import {addAliasRow} from '../alias-editor/actions'; -import {addIdentifierRow} from '../identifier-editor/actions'; +import {addAliasRow} from '../alias-section/actions'; import {connect} from 'react-redux'; /** * Container component. This component renders three buttons in a horizontal * row allowing the user to open the AliasEditor (AliasButton), add a - * disambiguation to the entity and open the IdentifierEditor + * disambiguation to the entity and open the identifierSection * (IdentifierButton). * * @param {Object} props - The properties passed to the component. * @param {number} props.numAliases - The number of aliases present in * the AliasEditor - passed to the AliasButton component. * @param {number} props.numIdentifiers - The number of identifiers present in - * the IdentifierEditor - passed to the IdentiferButton component. + * the identifierSection - passed to the IdentiferButton component. * @param {Function} props.onAliasButtonClick - A function to be called when the * AliasButton is clicked. * @param {Function} props.onIdentifierButtonClick - A function to be called @@ -107,10 +106,10 @@ function mapStateToProps(rootState, {identifierTypes}) { return { aliasesInvalid: !validateAliases(rootState.get('aliasEditor')), identifiersInvalid: !validateIdentifiers( - rootState.get('identifierEditor'), identifierTypes + rootState.get('identifierSection'), identifierTypes ), numAliases: rootState.get('aliasEditor').size, - numIdentifiers: rootState.get('identifierEditor').size + numIdentifiers: rootState.get('identifierSection').size }; } @@ -122,7 +121,6 @@ function mapDispatchToProps(dispatch) { }, onIdentifierButtonClick: () => { dispatch(showIdentifierEditor()); - dispatch(addIdentifierRow()); } }; } diff --git a/src/client/entity-editor/button-bar/identifier-button.js b/src/client/entity-editor/button-bar/identifier-button.js index 15332ef063..d344f94c87 100644 --- a/src/client/entity-editor/button-bar/identifier-button.js +++ b/src/client/entity-editor/button-bar/identifier-button.js @@ -26,14 +26,14 @@ import {faTimes} from '@fortawesome/free-solid-svg-icons'; /** * Presentational component. The IdentifierButton component renders a button * component in the style of a link. The link text indicates the number of - * identifiers currently set in the IdentifierEditor, and invites the user to + * identifiers currently set in the identifierSection, and invites the user to * add new or edit existing identifiers. * * @param {Object} props - The properties passed to the component. * @param {boolean} props.identifiersInvalid - Whether the inputs are valid * identifiers. * @param {number} props.numIdentifiers - The number of identifiers present in - * the IdentifierEditor - used to determine the correct button label. + * the identifierSection - used to determine the correct button label. * @returns {ReactElement} React element containing the rendered * IdentifierButton. */ diff --git a/src/client/entity-editor/button-bar/reducer.js b/src/client/entity-editor/button-bar/reducer.js index 803fead144..4640b321b3 100644 --- a/src/client/entity-editor/button-bar/reducer.js +++ b/src/client/entity-editor/button-bar/reducer.js @@ -19,8 +19,7 @@ import { SHOW_ALIAS_EDITOR, SHOW_IDENTIFIER_EDITOR } from './actions'; -import {HIDE_ALIAS_EDITOR} from '../alias-editor/actions'; -import {HIDE_IDENTIFIER_EDITOR} from '../identifier-editor/actions'; +import {HIDE_ALIAS_EDITOR} from '../alias-section/actions'; import Immutable from 'immutable'; @@ -38,8 +37,6 @@ function reducer( return state.set('aliasEditorVisible', false); case SHOW_IDENTIFIER_EDITOR: return state.set('identifierEditorVisible', true); - case HIDE_IDENTIFIER_EDITOR: - return state.set('identifierEditorVisible', false); // no default } return state; diff --git a/src/client/entity-editor/entity-editor.tsx b/src/client/entity-editor/entity-editor.tsx index 317b6e32ba..03266d9902 100644 --- a/src/client/entity-editor/entity-editor.tsx +++ b/src/client/entity-editor/entity-editor.tsx @@ -18,11 +18,10 @@ import * as React from 'react'; import {connect, useSelector} from 'react-redux'; -import AliasEditor from './alias-editor/alias-editor'; +import AliasSection from './alias-section/alias-section'; import AnnotationSection from './annotation-section/annotation-section'; -import ButtonBar from './button-bar/button-bar'; import {Card} from 'react-bootstrap'; -import IdentifierEditor from './identifier-editor/identifier-editor'; +import IdentifierSection from './identifier-section/identifier-section'; import NameSection from './name-section/name-section'; import RelationshipSection from './relationship-editor/relationship-section'; import SubmissionSection from './submission-section/submission-section'; @@ -38,25 +37,16 @@ type OwnProps = { entity: any }; -type StateProps = { - aliasEditorVisible: boolean, - identifierEditorVisible: boolean, -}; - type DispatchProps = { onSubmit: (event:React.FormEvent) => unknown }; -type Props = StateProps & DispatchProps & OwnProps; +type Props = DispatchProps & OwnProps; /** * Container component. Renders all of the sections of the entity editing form. * * @param {Object} props - The properties passed to the component. - * @param {boolean} props.aliasEditorVisible - Whether the alias editor modal - * should be made visible. - * @param {boolean} props.identifierEditorVisible - Whether the identifier - * editor modal should be made visible. * @param {React.Node} props.children - The child content to wrap with this * entity editor form. * @param {Function} props.onSubmit - A function to be called when the @@ -65,10 +55,8 @@ type Props = StateProps & DispatchProps & OwnProps; */ const EntityEditor = (props: Props) => { const { - aliasEditorVisible, children, heading, - identifierEditorVisible, onSubmit, entity } = props; @@ -84,10 +72,10 @@ const EntityEditor = (props: Props) => { window.onbeforeunload = handleUrlChange; }, [handleUrlChange]); - if(entity){ - entityURL = getEntityUrl(entity); + if (entity) { + entityURL = getEntityUrl(entity); } - + return (
@@ -97,9 +85,8 @@ const EntityEditor = (props: Props) => { - - + { React.cloneElement( React.Children.only(children), @@ -107,7 +94,7 @@ const EntityEditor = (props: Props) => { ) } - + @@ -119,14 +106,6 @@ const EntityEditor = (props: Props) => { }; EntityEditor.displayName = 'EntityEditor'; -function mapStateToProps(rootState): StateProps { - const state = rootState.get('buttonBar'); - return { - aliasEditorVisible: state.get('aliasEditorVisible'), - identifierEditorVisible: state.get('identifierEditorVisible') - }; -} - function mapDispatchToProps(dispatch, {submissionUrl}) { return { onSubmit: (event:React.FormEvent) => { @@ -136,4 +115,4 @@ function mapDispatchToProps(dispatch, {submissionUrl}) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(EntityEditor); +export default connect(null, mapDispatchToProps)(EntityEditor); diff --git a/src/client/entity-editor/entity-merge.tsx b/src/client/entity-editor/entity-merge.tsx index c8ac5e29f1..e53dcd01a2 100644 --- a/src/client/entity-editor/entity-merge.tsx +++ b/src/client/entity-editor/entity-merge.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import {Card, Col, Row} from 'react-bootstrap'; -import AliasEditorMerge from './alias-editor/alias-editor-merge'; +import AliasEditorMerge from './alias-section/alias-editor-merge'; import AnnotationSection from './annotation-section/annotation-section'; import Entity from './common/entity'; import EntityIdentifiers from '../components/pages/entities/identifiers'; @@ -157,7 +157,7 @@ EntityMerge.displayName = 'EntityMerge'; function mapStateToProps(rootState): StateProps { return { - identifierSet: rootState.get('identifierEditor', {}) + identifierSet: rootState.get('identifierSection', {}) }; } function mapDispatchToProps(dispatch, {submissionUrl}) { diff --git a/src/client/entity-editor/helpers.ts b/src/client/entity-editor/helpers.ts index 882fe30146..6cbf9415e4 100644 --- a/src/client/entity-editor/helpers.ts +++ b/src/client/entity-editor/helpers.ts @@ -30,18 +30,17 @@ import SeriesSection from './series-section/series-section'; import SeriesSectionMerge from './series-section/series-section-merge'; import WorkSection from './work-section/work-section'; import WorkSectionMerge from './work-section/work-section-merge'; -import aliasEditorReducer from './alias-editor/reducer'; +import aliasSectionReducer from './alias-section/reducer'; import annotationSectionReducer from './annotation-section/reducer'; import authorCreditEditorReducer from './author-credit-editor/reducer'; import authorCreditMergeReducer from './author-credit-editor/merge-reducer'; import authorSectionReducer from './author-section/reducer'; -import buttonBarReducer from './button-bar/reducer'; import {camelCase} from 'lodash'; import {combineReducers} from 'redux-immutable'; import editionGroupSectionReducer from './edition-group-section/reducer'; import editionSectionReducer from './edition-section/reducer'; import {getAttributeName} from './relationship-editor/helper'; -import identifierEditorReducer from './identifier-editor/reducer'; +import identifierSectionReducer from './identifier-section/reducer'; import nameSectionReducer from './name-section/reducer'; import publisherSectionReducer from './publisher-section/reducer'; import relationshipSectionReducer from './relationship-editor/reducer'; @@ -135,11 +134,10 @@ export function createRootReducer(entityType: string, isMerge = false) { const entityReducer = getEntitySectionReducer(entityType); const reducers = { - aliasEditor: aliasEditorReducer, + aliasSection: aliasSectionReducer, annotationSection: annotationSectionReducer, - buttonBar: buttonBarReducer, [entityReducerKey]: entityReducer, - identifierEditor: identifierEditorReducer, + identifierSection: identifierSectionReducer, nameSection: nameSectionReducer, relationshipSection: relationshipSectionReducer, submissionSection: submissionSectionReducer diff --git a/src/client/entity-editor/identifier-editor/identifier-modal-body.tsx b/src/client/entity-editor/identifier-editor/identifier-modal-body.tsx deleted file mode 100644 index 88f2ba60f1..0000000000 --- a/src/client/entity-editor/identifier-editor/identifier-modal-body.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import {Button, Col, Row} from 'react-bootstrap'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import IdentifierRow from './identifier-row'; -import Immutable from 'immutable'; -import React from 'react'; -import {addIdentifierRow} from './actions'; -import classNames from 'classnames'; -import {connect} from 'react-redux'; -import {faPlus} from '@fortawesome/free-solid-svg-icons'; - - -type IdentifierModalBodyStateProps = { - identifiers: Immutable.Map; -}; -type IdentifierModalBodyDispatchProps = { - onAddIdentifier: () => void -}; -type IdentifierModalBodyOwnProps = { - identifierTypes: Array; -}; -type IdentifierModalBodyProps = IdentifierModalBodyOwnProps & IdentifierModalBodyStateProps & IdentifierModalBodyDispatchProps; - - -export const IdentifierModalBody = ({identifiers, onAddIdentifier, identifierTypes}:IdentifierModalBodyProps) => { - const noIdentifiersTextClass = - classNames('text-center', {'d-none': identifiers.size}); - - return ( - <> -
-

This entity has no identifiers

-
-
- { - identifiers.map((identifier, rowId) => ( - - )).toArray() - } -
- - - - - - ); -}; - -function mapStateToProps(state) { - return { - identifiers: state.get('identifierEditor') - }; -} - -function mapDispatchToProps(dispatch) { - return { - onAddIdentifier: () => dispatch(addIdentifierRow()) - }; -} -export default connect(mapStateToProps, mapDispatchToProps)(IdentifierModalBody); diff --git a/src/client/entity-editor/identifier-editor/actions.ts b/src/client/entity-editor/identifier-section/actions.ts similarity index 86% rename from src/client/entity-editor/identifier-editor/actions.ts rename to src/client/entity-editor/identifier-section/actions.ts index 2dc897deba..4d524aa96c 100644 --- a/src/client/entity-editor/identifier-editor/actions.ts +++ b/src/client/entity-editor/identifier-section/actions.ts @@ -16,13 +16,12 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -export const ADD_IDENTIFIER_ROW = 'ADD_IDENTIFIER_ROW'; export const REMOVE_IDENTIFIER_ROW = 'REMOVE_IDENTIFIER_ROW'; export const UPDATE_IDENTIFIER_TYPE = 'UPDATE_IDENTIFIER_TYPE'; export const UPDATE_IDENTIFIER_VALUE = 'UPDATE_IDENTIFIER_VALUE'; -export const HIDE_IDENTIFIER_EDITOR = 'HIDE_IDENTIFIER_EDITOR'; export const REMOVE_EMPTY_IDENTIFIERS = 'REMOVE_EMPTY_IDENTIFIERS'; export const ADD_OTHER_ISBN = 'ADD_OTHER_ISBN'; +export const ADD_IDENTIFIER = 'ADD_IDENTIFIER'; export type Action = { type: string, @@ -32,20 +31,6 @@ export type Action = { } }; -/** - * Produces an action indicating that the identifier editor should be hidden - * from view. - * - * @see showIdentifierEditor - * - * @returns {Action} The resulting HIDE_IDENTIFIER_EDITOR action. - */ -export function hideIdentifierEditor(): Action { - return { - type: HIDE_IDENTIFIER_EDITOR - }; -} - let nextIdentifierRowId = 0; /** @@ -55,14 +40,20 @@ let nextIdentifierRowId = 0; * * @returns {Action} The resulting ADD_IDENTIFIER_ROW action. */ -export function addIdentifierRow(): Action { - /* - * Prepend 'n' here to indicate new identifier, and avoid conflicts with IDs - * of existing identifiers. - */ + +export function addIdentifier( + value: string, type: {value: number, label: string}, suggestedType: number +): Action { return { - payload: `n${nextIdentifierRowId++}`, - type: ADD_IDENTIFIER_ROW + payload: { + data: { + type: type.value, + value + }, + rowId: `n${nextIdentifierRowId++}`, + suggestedType + }, + type: ADD_IDENTIFIER }; } diff --git a/src/client/entity-editor/identifier-section/identifier-editor.tsx b/src/client/entity-editor/identifier-section/identifier-editor.tsx new file mode 100644 index 0000000000..fe74867fd5 --- /dev/null +++ b/src/client/entity-editor/identifier-section/identifier-editor.tsx @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 Ben Ockmore + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {Button, Col, Form, Modal, Row} from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Select from 'react-select'; +import ValueField from './value-field'; +import {connect} from 'react-redux'; +import {removeEmptyIdentifiers} from './actions'; +import {validateIdentifierValue} from '../validators/common'; + + +const onClose = (setVisibility, onRemoveEmptyIdentifiers) => { + setVisibility(false); + onRemoveEmptyIdentifiers(); +}; + +const identifierSection = ({ + index, + identifierTypesForDisplay, + identifierValue, + onRemoveEmptyIdentifiers, + onTypeChange, + onValueChange, + typeOptions, + valueValue, + typeValue, + show, + setVisibility +}) => { + const handleModalCLose = React.useCallback(() => { + onClose(setVisibility, onRemoveEmptyIdentifiers); + }, []); + return ( + + + + Identifier Editor + + + + + + + + + + + Type + + + + + + + + + ); +} +IdentifierFields.displayName = 'IdentifierFields'; + + +function handleValueChange( + dispatch: Dispatch, + orignalValue: string, + type: TypeObj, + types: Array +) { + let value = collapseWhiteSpaces(orignalValue); + const guessedType = + data.guessIdentifierType(value, types); + if (guessedType) { + const result = new RegExp(guessedType.detectionRegex).exec(value); + value = result[1]; + // disabling "add isbn row" feature temporary + // if (guessedType.id === 9) { + // const isbn10Type:any = types.find((el) => el.id === 10); + // const isbn10 = isbn13To10(value); + // if (isbn10) { + // dispatch(debouncedUpdateIdentifierValue(index + 1, isbn10, isbn10Type, false)); + // } + // } + // if (guessedType.id === 10) { + // const isbn13Type:any = types.find((el) => el.id === 9); + // const isbn13 = isbn10To13(value); + // if (isbn13) { + // dispatch(debouncedUpdateIdentifierValue(index + 1, isbn10To13(value), isbn13Type, false)); + // } + // } + } + return dispatch(addIdentifier(value, type, guessedType)); +} + +function mapDispatchToProps( + dispatch: Dispatch, + {typeOptions}: OwnProps +): DispatchProps { + return { + onAddNewIdentifier: (value, type) => { + handleValueChange(dispatch, value, type, typeOptions); + } + }; +} + +export default connect(null, mapDispatchToProps)(IdentifierFields); diff --git a/src/client/entity-editor/identifier-editor/identifier-row.tsx b/src/client/entity-editor/identifier-section/identifier-row.tsx similarity index 70% rename from src/client/entity-editor/identifier-editor/identifier-row.tsx rename to src/client/entity-editor/identifier-section/identifier-row.tsx index 285d66ba3a..f1e0dac0dc 100644 --- a/src/client/entity-editor/identifier-editor/identifier-row.tsx +++ b/src/client/entity-editor/identifier-section/identifier-row.tsx @@ -24,24 +24,20 @@ import { Action, debouncedUpdateIdentifierValue, removeIdentifierRow, updateIdentifierType } from './actions'; -import {Button, Col, Form, Row} from 'react-bootstrap'; -import { - IdentifierType, - validateIdentifierValue -} from '../validators/common'; +import {Col, Row} from 'react-bootstrap'; +import {faPencilAlt, faTrash} from '@fortawesome/free-solid-svg-icons'; import type {Dispatch} from 'redux'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import Select from 'react-select'; -import ValueField from './value-field'; +import IdentifierEditor from './identifier-editor'; +import {IdentifierType} from '../validators/common'; import {collapseWhiteSpaces} from '../../../common/helpers/utils'; import {connect} from 'react-redux'; -import {faTimes} from '@fortawesome/free-solid-svg-icons'; -import IdentifierLink from "../../components/pages/entities/identifiers-links.js" type OwnProps = { index: number, - typeOptions: Array + typeOptions: Array, + isUnifiedForm: boolean }; type StateProps = { @@ -55,14 +51,14 @@ type DispatchProps = { onValueChange: (arg: React.ChangeEvent) => unknown }; -type Props = StateProps & DispatchProps & OwnProps; +type Props = DispatchProps & OwnProps & StateProps; /** - * Container component. The IdentifierRow component renders a single Row - * containing several input fields, allowing the user to set the value and type - * for an identifier in the IdentifierEditor. A button is also included to - * remove the identifier from the editor. - * + * Container component. The IdentifierRow component renders all the identifiers + * of the entity, displaying information about the identifier value and it's type + * with the edit and delete icon button besides each identifiers + * the edit icon button opens a model when the show property is set + * the delete icon button removes the identifier from the list when clicked * @param {Object} props - The properties passed to the component. * @param {number} props.index - The index of the row in the parent editor. * @param {Array} props.typeOptions - The list of possible types for an @@ -73,7 +69,7 @@ type Props = StateProps & DispatchProps & OwnProps; * @param {Function} props.onTypeChange - A function to be called when a new * identifier type is selected. * @param {Function} props.onRemoveButtonClick - A function to be called when - * the button to remove the identifier is clicked. + * the trash icon to remove the identifier is clicked. * @param {Function} props.onValueChange - A function to be called when the * value for the identifier is changed. * @returns {ReactElement} React element containing the rendered IdentifierRow. @@ -85,63 +81,60 @@ function IdentifierRow({ typeValue, onTypeChange, onRemoveButtonClick, - onValueChange + onValueChange, + isUnifiedForm }: Props) { const identifierTypesForDisplay = typeOptions.map((type) => ({ label: type.label, value: type.id })); + const [showEditor, setEditorVisibility] = React.useState(false); const identifierValue = identifierTypesForDisplay.filter((el) => el.value === typeValue); + + const handleEditButtonClick = React.useCallback(() => { + setEditorVisibility(true); + }, []); + + const lgCol = {offset: 4, span: 3}; + if (isUnifiedForm) { + lgCol.offset = 0; + } return (
+ - - - - - - Type -