From de5469b8af7aab25e82685da6640dc69045b1c37 Mon Sep 17 00:00:00 2001 From: Yurii Danylenko <43621626+Yurii-Danylenko@users.noreply.github.com> Date: Tue, 14 May 2019 15:50:11 +0300 Subject: [PATCH] STSMACOM-196: Add notes pop-up (#500) * STSMACOM-196: Add notes pop-up --- index.js | 3 +- lib/Notes/NotesAccordion/NotesAccordion.css | 2 +- lib/Notes/NotesAccordion/NotesAccordion.js | 142 +++---- .../NotesAssigningModal.css | 26 ++ .../NotesAssigningModal.js | 388 ++++++++++++++++++ .../NotesAssigningModal/constants.js | 7 + .../components/NotesAssigningModal/index.js | 1 + .../components/NotesList/NotesList.js | 83 +++- lib/Notes/NotesAccordion/components/index.js | 3 +- lib/Notes/NotesAccordion/constants.js | 9 + lib/Notes/NotesAccordion/index.js | 1 + lib/Notes/index.js | 2 + translations/stripes-smart-components/en.json | 10 +- 13 files changed, 585 insertions(+), 92 deletions(-) create mode 100644 lib/Notes/NotesAccordion/components/NotesAssigningModal/NotesAssigningModal.css create mode 100644 lib/Notes/NotesAccordion/components/NotesAssigningModal/NotesAssigningModal.js create mode 100644 lib/Notes/NotesAccordion/components/NotesAssigningModal/constants.js create mode 100644 lib/Notes/NotesAccordion/components/NotesAssigningModal/index.js create mode 100644 lib/Notes/NotesAccordion/constants.js create mode 100644 lib/Notes/NotesAccordion/index.js create mode 100644 lib/Notes/index.js diff --git a/index.js b/index.js index b158b63ad..da565e9ec 100644 --- a/index.js +++ b/index.js @@ -53,4 +53,5 @@ export { default as UserName } from './lib/UserName'; export { default as ViewMetaData } from './lib/ViewMetaData'; export { default as DueDatePicker } from './lib/ChangeDueDateDialog/DueDatePicker'; -export { default as NoteForm } from './lib/Notes/NoteForm'; + +export * from './lib/Notes'; diff --git a/lib/Notes/NotesAccordion/NotesAccordion.css b/lib/Notes/NotesAccordion/NotesAccordion.css index e67f1b672..405f6c628 100644 --- a/lib/Notes/NotesAccordion/NotesAccordion.css +++ b/lib/Notes/NotesAccordion/NotesAccordion.css @@ -1,3 +1,3 @@ a.new-button { - margin-left: 10px; + margin-left: 15px; } diff --git a/lib/Notes/NotesAccordion/NotesAccordion.js b/lib/Notes/NotesAccordion/NotesAccordion.js index 032eecbb1..f0163a368 100644 --- a/lib/Notes/NotesAccordion/NotesAccordion.js +++ b/lib/Notes/NotesAccordion/NotesAccordion.js @@ -1,8 +1,7 @@ +/* eslint-disable react/prop-types */ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; -import { - FormattedMessage, -} from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import { Accordion, @@ -11,92 +10,65 @@ import { Button, } from '@folio/stripes-components'; -import NotesList from './components'; +import NotesList from './components/NotesList'; +import NotesAssigningModal from './components/NotesAssigningModal'; + import styles from './NotesAccordion.css'; -const defaultColumnsConfig = [ - { - name: 'date', - title: , - width: '30%', - }, - { - name: 'updatedBy', - title: , - width: '30%', - }, - { - name: 'title', - title: , - width: '40%', - }, -]; - -export const columnShape = PropTypes.shape({ - name: PropTypes.string.isRequired, - title: PropTypes.node.isRequired, - width: PropTypes.string, -}); - -export const noteShape = PropTypes.shape({ - id: PropTypes.string, - lastSavedDate: PropTypes.instanceOf(Date), - lastSavedUserFullName: PropTypes.string, - title: PropTypes.string, -}); - -export class NotesAccordion extends Component { +export default class NotesAccordion extends Component { static propTypes = { - columnsConfig: PropTypes.arrayOf(columnShape), - notes: PropTypes.arrayOf(noteShape), onCreate: PropTypes.func.isRequired, - onNoteClick: PropTypes.func.isRequired, + onSaveAssigningResults: PropTypes.func.isRequired, open: PropTypes.bool, - } + }; + + state = { modalIsOpen: false }; - static defaultProps = { - columnsConfig: defaultColumnsConfig, - notes: [], + onCloseModal = () => { + this.setState({ + modalIsOpen: false, + }); + }; + + onSaveAssigningResults = (changedNoteIdToStatusMap) => { + this.props.onSaveAssigningResults(changedNoteIdToStatusMap); + this.onCloseModal(); } renderHeader = () => { return ( - + ); - } + }; renderHeaderButtons() { - const { - onCreate, - } = this.props; + const { onCreate } = this.props; return ( - - ); } + onAssignButtonClick = () => { + this.setState({ + modalIsOpen: true, + }); + }; + renderQuantityIndicator() { return ( - - {this.props.notes.length} + + {this.props.assignedNotes.items.length} ); @@ -106,24 +78,46 @@ export class NotesAccordion extends Component { const { columnsConfig, open, - notes, + assignedNotes, + domainNotes, onNoteClick, + onResetSearchResults, + onSearch, + onSortDomainNotes, + onNeedMoreDomainNotes, } = this.props; + const { + modalIsOpen, + } = this.state; + return ( - - + + + + + - + ); } } diff --git a/lib/Notes/NotesAccordion/components/NotesAssigningModal/NotesAssigningModal.css b/lib/Notes/NotesAccordion/components/NotesAssigningModal/NotesAssigningModal.css new file mode 100644 index 000000000..ecaf1e977 --- /dev/null +++ b/lib/Notes/NotesAccordion/components/NotesAssigningModal/NotesAssigningModal.css @@ -0,0 +1,26 @@ +@import "@folio/stripes-components/lib/variables.css"; + +.assign-checkbox label { + margin-bottom: 0; +} + +.assign-checkbox input { + margin: 0; +} + +.assign-checkbox label::before { + margin-left: 10px; + margin-right: 10px; +} + +.assign-checkbox label::after { + content: none; +} + +.save-button { + margin-right: var(--gutter-static); +} + +.search-field { + margin-bottom: var(--gutter-static); +} diff --git a/lib/Notes/NotesAccordion/components/NotesAssigningModal/NotesAssigningModal.js b/lib/Notes/NotesAccordion/components/NotesAssigningModal/NotesAssigningModal.js new file mode 100644 index 000000000..ac41329d4 --- /dev/null +++ b/lib/Notes/NotesAccordion/components/NotesAssigningModal/NotesAssigningModal.js @@ -0,0 +1,388 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + Modal, + Paneset, + Pane, + MultiColumnList, + Button, + SearchField, + Checkbox, + Accordion, + Headline, + IconButton, +} from '@folio/stripes-components'; + +import { SearchAndSortResetButton as ResetButton, CheckboxFilter } from '../../../../..'; + +import { + notesStatuses, + sortOrders, +} from '../../constants'; +import { + columnNames, + columnWidths, +} from './constants'; + +import styles from './NotesAssigningModal.css'; + +const { + ASSIGNED, + UNASSIGNED, +} = notesStatuses; + +const { + ASC, + DESC, +} = sortOrders; + +const { + ASSIGNING, + TITLE, + STATUS, +} = columnNames; + +const notesStatusOptions = [ + { + label: , + value: ASSIGNED, + }, + { + label: , + value: UNASSIGNED, + }, +]; + +const noteShape = PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.oneOf([ASSIGNED, UNASSIGNED]), + title: PropTypes.node.isRequired, +}); + +const notesShape = PropTypes.shape({ + items: PropTypes.arrayOf(noteShape), + loading: PropTypes.bool, + sortParams: PropTypes.shape({ + by: PropTypes.string, + order: PropTypes.oneOf([ASC, DESC]), + }), + totalCount: PropTypes.number, +}); + +export default class NotesAssigningModal extends React.Component { + static propTypes = { + notes: notesShape, + onClose: PropTypes.func.isRequired, + onNeedMore: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + onSearch: PropTypes.func.isRequired, + onSort: PropTypes.func.isRequired, + open: PropTypes.bool, + }; + + static defaultProps = { + notes: { + totalCount: 0, + items: [], + sortParams: { + order: ASC, + by: TITLE, + }, + loading: false, + }, + open: false, + }; + + state = { + selectedStatusFilters: [], + searchQuery: '', + changedNoteIdToStatusMap: new Map(), + }; + + firstColumnHeaderFormatter = { + [ASSIGNING]: (record) => { + const isAssigned = record.status === ASSIGNED; + + return ( + this.onSingleAssignClick(record)} + /> + ); + }, + }; + + onSingleAssignClick = (note) => { + this.setState((state, props) => { + return { + changedNoteIdToStatusMap: this.getNewChangedNoteIdToStatusMap(state, note, props.notes.items), + }; + }); + }; + + getNewChangedNoteIdToStatusMap = (state, note, notes) => { + const { changedNoteIdToStatusMap } = state; + + const incomingNoteStatus = notes.find((curNote) => curNote.id === note.id).status; + const newStatus = note.status === ASSIGNED ? UNASSIGNED : ASSIGNED; + const newChangedNoteIdToStatusMap = new Map(changedNoteIdToStatusMap); + + if (incomingNoteStatus === newStatus) { + newChangedNoteIdToStatusMap.delete(note.id); + } else { + newChangedNoteIdToStatusMap.set(note.id, newStatus); + } + + return newChangedNoteIdToStatusMap; + }; + + onStatusFilterChange = (filter) => { + this.setState( + { selectedStatusFilters: [...filter.values] }, + this.search + ); + }; + + getNotes = () => { + const { changedNoteIdToStatusMap } = this.state; + + return this.props.notes.items.map((curNote) => { + return changedNoteIdToStatusMap.has(curNote.id) + ? { ...curNote, status: changedNoteIdToStatusMap.get(curNote.id) } + : curNote; + }); + }; + + renderFooter() { + return ( + + + + + ); + } + + onSaveAndClose = () => { + this.props.onSave(this.state.changedNoteIdToStatusMap); + } + + search = () => { + const { + searchQuery, + selectedStatusFilters, + } = this.state; + + this.props.onSearch({ + query: searchQuery, + assignmentStatuses: selectedStatusFilters, + }); + } + + onSearchQueryChange = (event) => { + this.setState({ searchQuery: event.target.value }); + } + + onResetAll = () => { + this.setState({ + searchQuery: '', + selectedStatusFilters: [], + }, this.search); + } + + onColumnHeaderClick = (event, column) => { + if (column.name === ASSIGNING) return; + + this.props.onSort(this.getSortParams(column.name)); + } + + getSortParams = (columnName) => { + const { + notes: { sortParams } + } = this.props; + const columnIsChanged = columnName !== sortParams.by; + + const sortOrder = columnIsChanged + ? ASC + : sortParams.order === ASC ? DESC : ASC; + + return { + order: sortOrder, + by: columnName, + }; + }; + + getColumnMapping = () => { + return { + [ASSIGNING]: ( + + ), + title: , + status: , + }; + } + + onAssignAllClick = (event) => { + const { checked } = event.target; + + this.setState((state) => { + const newStatus = checked ? ASSIGNED : UNASSIGNED; + const newChangedNoteIdToStatusMap = new Map(state.changedNoteIdToStatusMap); + + this.props.notes.items.forEach((curIncomingNote) => { + if (curIncomingNote.status !== newStatus) { + newChangedNoteIdToStatusMap.set(curIncomingNote.id, newStatus); + } else { + newChangedNoteIdToStatusMap.delete(curIncomingNote.id); + } + }); + + return { + changedNoteIdToStatusMap: newChangedNoteIdToStatusMap, + }; + }); + } + + renderResetFiltersButton() { + return ( + + ); + } + + onFiltersReset = () => { + this.setState({ + selectedStatusFilters: [], + }); + + this.props.onSearch({ + assignmentStatuses: [], + }); + } + + render() { + const { + open, + notes, + onClose, + onNeedMore, + } = this.props; + + const { + searchQuery, + selectedStatusFilters, + } = this.state; + + const searchQueryIsEmpty = searchQuery.length === 0; + const statusFiltersAreEmpty = selectedStatusFilters.length === 0; + const searchOptionsIsEmpty = searchQueryIsEmpty && statusFiltersAreEmpty; + const thereAreSelectedFilters = selectedStatusFilters.length > 0; + + return ( + } + onClose={onClose} + footer={this.renderFooter()} + dismissible + > + + } + > + } + name="query" + onChange={this.onSearchQueryChange} + value={searchQuery} + marginBottom0 + autoFocus + className={styles['search-field']} + /> + +
+ } + disabled={searchOptionsIsEmpty} + onClick={this.onResetAll} + /> +
+ + + +
+ } + displayWhenOpen={thereAreSelectedFilters && this.renderResetFiltersButton()} + > + + + + } + paneSub={( + + )} + defaultWidth="70%" + > + } + formatter={this.firstColumnHeaderFormatter} + virtualize + onNeedMoreData={onNeedMore} + loading={notes.loading} + /> + + + + ); + } +} diff --git a/lib/Notes/NotesAccordion/components/NotesAssigningModal/constants.js b/lib/Notes/NotesAccordion/components/NotesAssigningModal/constants.js new file mode 100644 index 000000000..d328419a4 --- /dev/null +++ b/lib/Notes/NotesAccordion/components/NotesAssigningModal/constants.js @@ -0,0 +1,7 @@ +export const columnNames = { + ASSIGNING: 'assigning', + TITLE: 'title', + STATUS: 'status', +}; + +export const columnWidths = { [columnNames.ASSIGNING]: '5%' }; diff --git a/lib/Notes/NotesAccordion/components/NotesAssigningModal/index.js b/lib/Notes/NotesAccordion/components/NotesAssigningModal/index.js new file mode 100644 index 000000000..3fd1ab454 --- /dev/null +++ b/lib/Notes/NotesAccordion/components/NotesAssigningModal/index.js @@ -0,0 +1 @@ +export { default } from './NotesAssigningModal'; diff --git a/lib/Notes/NotesAccordion/components/NotesList/NotesList.js b/lib/Notes/NotesAccordion/components/NotesList/NotesList.js index b671270d2..eb064831c 100644 --- a/lib/Notes/NotesAccordion/components/NotesList/NotesList.js +++ b/lib/Notes/NotesAccordion/components/NotesList/NotesList.js @@ -6,20 +6,70 @@ import { } from 'react-intl'; import { MultiColumnList } from '@folio/stripes-components'; + import { - noteShape, - columnShape, -} from '../../NotesAccordion'; + notesStatuses, +} from '../../constants'; + +const defaultColumnsConfig = [ + { + name: 'date', + title: , + width: '30%', + }, + { + name: 'updatedBy', + title: , + width: '30%', + }, + { + name: 'title', + title: , + width: '40%', + }, +]; + +const { + ASSIGNED, + UNASSIGNED, +} = notesStatuses; + +const noteShape = PropTypes.shape({ + id: PropTypes.string, + lastSavedDate: PropTypes.instanceOf(Date), + lastSavedUserFullName: PropTypes.string, + title: PropTypes.string, +}); + +const columnsConfigShape = PropTypes.shape({ + items: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + status: PropTypes.oneOf([ASSIGNED, UNASSIGNED]), + title: PropTypes.string, + })), + totalCount: PropTypes.number, +}); export default class NotesList extends React.Component { static propTypes = { - columnsConfig: PropTypes.arrayOf(columnShape).isRequired, - notes: PropTypes.arrayOf(noteShape).isRequired, + columnsConfig: PropTypes.arrayOf(columnsConfigShape), + notes: PropTypes.shape({ + items: PropTypes.arrayOf(noteShape), + loading: PropTypes.bool, + }), onNoteClick: PropTypes.func.isRequired, } + static defaultProps = { + columnsConfig: defaultColumnsConfig, + notes: { + items: [], + loading: false, + }, + } + getItems() { - return this.props.notes + return this.props.notes.items .map(note => { const { id, @@ -49,17 +99,22 @@ export default class NotesList extends React.Component { } getColumnTitlesMap() { - return this.props.columnsConfig.reduce((columnsMap, { name, title }) => { - columnsMap[name] = title; - return columnsMap; - }); + return this.props.columnsConfig + .reduce((columnsMap, { name, title }) => { + columnsMap[name] = title; + return columnsMap; + }, {}); } getColumnWidths() { - return this.props.columnsConfig.map(({ name, width }) => ({ [name]: width })); + return this.props.columnsConfig + .reduce((columnsWidths, { name, width }) => { + columnsWidths[name] = width; + return columnsWidths; + }, {}); } - onRowClickHandler = (event, note) => { + onRowClick= (event, note) => { this.props.onNoteClick(note.id); } @@ -70,7 +125,6 @@ export default class NotesList extends React.Component { (ariaLabel) => ( } - onRowClick={this.onRowClickHandler} + onRowClick={this.onRowClick} + loading={this.props.notes.loading} /> ) } diff --git a/lib/Notes/NotesAccordion/components/index.js b/lib/Notes/NotesAccordion/components/index.js index e6078c6f5..4d1ea818f 100644 --- a/lib/Notes/NotesAccordion/components/index.js +++ b/lib/Notes/NotesAccordion/components/index.js @@ -1 +1,2 @@ -export { default } from './NotesList'; +export { default as NotesList } from './NotesList'; +export { default as NotesAssigningModal } from './NotesAssigningModal'; diff --git a/lib/Notes/NotesAccordion/constants.js b/lib/Notes/NotesAccordion/constants.js new file mode 100644 index 000000000..110a1c258 --- /dev/null +++ b/lib/Notes/NotesAccordion/constants.js @@ -0,0 +1,9 @@ +export const notesStatuses = { + ASSIGNED: 'assigned', + UNASSIGNED: 'unassigned', +}; + +export const sortOrders = { + ASC: 'ascending', + DESC: 'descending', +}; diff --git a/lib/Notes/NotesAccordion/index.js b/lib/Notes/NotesAccordion/index.js new file mode 100644 index 000000000..30e0c15ef --- /dev/null +++ b/lib/Notes/NotesAccordion/index.js @@ -0,0 +1 @@ +export { default } from './NotesAccordion'; diff --git a/lib/Notes/index.js b/lib/Notes/index.js new file mode 100644 index 000000000..b749ca1ce --- /dev/null +++ b/lib/Notes/index.js @@ -0,0 +1,2 @@ +export { default as NotesAccordion } from './NotesAccordion'; +export { default as NoteForm } from './NoteForm'; diff --git a/translations/stripes-smart-components/en.json b/translations/stripes-smart-components/en.json index a0852a7b9..3af205fc2 100644 --- a/translations/stripes-smart-components/en.json +++ b/translations/stripes-smart-components/en.json @@ -76,6 +76,7 @@ "addNew": "Add new", "new": "New", "add": "Add", + "assign": "Assign", "hideSearchPane": "Hide search and filters pane.", "showSearchPane": "Show search and filters pane.", "numberOfFilters": "{count, number} {count, plural, one {applied filter} other {applied filters}}", @@ -112,6 +113,10 @@ "system": "System", "notes": "Notes", "notes.notFound": "No notes found", + "notes.assignNote": "Assign note", + "notes.noteSearch": "Note search", + "notes.noteAssignmentStatus": "Note assignment status", + "notes.found": "{quantity} notes found", "date": "Date", "title": "Title", "updatedBy": "Updated by", @@ -129,5 +134,8 @@ "notes.title.lengthLimitExceeded": "Note title character limit has been exceeded", "notes.details.lengthLimitExceeded": "Note details character limit has been exceeded", "notes.title.invalidCharacter": "Note title does not accept the following characters: {characters}", - "notes.details.invalidCharacter": "Note details does not accept the following characters: {characters}" + "notes.details.invalidCharacter": "Note details does not accept the following characters: {characters}", + "status": "Status", + "assigned": "Assigned", + "unassigned": "Unassigned" }