diff --git a/src/components/ResponseDisplay.jsx b/src/components/ResponseDisplay.jsx new file mode 100644 index 0000000..3a38b41 --- /dev/null +++ b/src/components/ResponseDisplay.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { + Card, +} from '@edx/paragon'; + +import createDOMPurify from 'dompurify'; + +import parse from 'html-react-parser'; + +import selectors from 'data/selectors'; + +/** + * + */ +export class ResponseDisplay extends React.Component { + constructor(props) { + super(props); + this.purify = createDOMPurify(window); + } + + get textContent() { + return parse(this.purify.sanitize(this.props.response.text)); + } + + get hasResponse() { + return this.props.response !== undefined; + } + + render() { + return ( + + {this.hasResponse && ( + + {this.textContent} + + )} + + ); + } +} + +ResponseDisplay.defaultProps = { +}; +ResponseDisplay.propTypes = { + response: PropTypes.shape({ + text: PropTypes.string, + }).isRequired, +}; + +export const mapStateToProps = (state) => ({ + response: selectors.grading.selectedResponse(state), +}); + +export const mapDispatchToProps = { +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ResponseDisplay); diff --git a/src/containers/ListView/index.jsx b/src/containers/ListView/index.jsx index 0d2bc2c..0d2c9d5 100644 --- a/src/containers/ListView/index.jsx +++ b/src/containers/ListView/index.jsx @@ -85,7 +85,7 @@ export class ListView extends React.Component { }, { Header: 'Grade', - accessor: 'grade', + accessor: 'score', Cell: this.formatGrade, }, { diff --git a/src/containers/ReviewModal/ReviewActions.jsx b/src/containers/ReviewModal/ReviewActions.jsx index a1ce267..b280fdc 100644 --- a/src/containers/ReviewModal/ReviewActions.jsx +++ b/src/containers/ReviewModal/ReviewActions.jsx @@ -6,33 +6,51 @@ import { ActionRow, Button, } from '@edx/paragon'; +import { Edit } from '@edx/paragon/icons'; +import actions from 'data/actions'; import selectors from 'data/selectors'; import StatusBadge from 'components/StatusBadge'; +import SubmissionNavigation from './SubmissionNavigation'; import './ReviewModal.scss'; -export const ReviewActions = ({ gradeStatus, username }) => ( +export const ReviewActions = ({ + gradeStatus, + toggleShowRubric, + showRubric, + username, +}) => (
- {username} - + + {username} + + - + + +
); ReviewActions.propTypes = { gradeStatus: PropTypes.string.isRequired, username: PropTypes.string.isRequired, + showRubric: PropTypes.bool.isRequired, + toggleShowRubric: PropTypes.func.isRequired, }; export const mapStateToProps = (state) => ({ username: selectors.grading.selected.username(state), gradeStatus: selectors.grading.selected.gradeStatus(state), + showRubric: selectors.app.showRubric(state), }); export const mapDispatchToProps = { + toggleShowRubric: actions.app.toggleShowRubric, }; export default connect(mapStateToProps, mapDispatchToProps)(ReviewActions); diff --git a/src/containers/ReviewModal/ReviewModal.scss b/src/containers/ReviewModal/ReviewModal.scss index 5dac7c4..99cd973 100644 --- a/src/containers/ReviewModal/ReviewModal.scss +++ b/src/containers/ReviewModal/ReviewModal.scss @@ -1,7 +1,84 @@ @import "@edx/paragon/scss/core/core"; -.review-actions { - padding: map_get($spacers, 3); + + +@include media-breakpoint-down(md) { + .review-actions { + flex-direction: column; + align-items: flex-start !important; + + & > * { + margin-bottom: map_get($spacers, 1) !important; + } + } } -#ora-esg-list-view { - padding: 20px; + +.review-modal { + background-color: $gray-300 !important; + padding: inherit; + + // action reviews + .review-actions { + padding: map_get($spacers, 3); + flex-direction: row; + background-color: $light-200; + + .review-actions-username { + flex-grow: 1; + } + } + // text response + .response-card { + padding: map-get($map: $spacers, $key: 0); + max-width: map-get($map: $container-max-widths, $key: "sm"); + height: 100%; + overflow-y: scroll; + } +} + +.content-block { + width: fit-content; + margin: auto; + height: 100%; +} + +@include media-breakpoint-down(sm) { + .fullscreen-modal-body { + padding: 0 !important; + overflow-y: hidden !important; + + & > div:nth-child(2) { + height: 100%; + + .row, + .col { + height: 100%; + padding: 0; + } + } + } +} + +.grading-rubric-card { + width: 320px; + height: fit-content; + max-height: 70vh; + min-height: 320px; + + .grading-rubric-header { + box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.3) !important; + display: flex; + justify-content: center; + padding: map-get($map: $spacers, $key: 3); + } + + .grading-rubric-body { + overflow-y: scroll; + } + + .grading-rubric-footer { + box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.3) !important; + display: flex; + justify-content: center; + padding: map-get($map: $spacers, $key: 3); + } } diff --git a/src/containers/ReviewModal/SubmissionNavigation.jsx b/src/containers/ReviewModal/SubmissionNavigation.jsx new file mode 100644 index 0000000..680231b --- /dev/null +++ b/src/containers/ReviewModal/SubmissionNavigation.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { Icon, IconButton } from '@edx/paragon'; +import { ChevronLeft, ChevronRight } from '@edx/paragon/icons'; + +import selectors from 'data/selectors'; +import thunkActions from 'data/thunkActions'; + +/** + * + */ +export const SubmissionNavigation = ({ + hasPrevSubmission, + hasNextSubmission, + loadPrev, + loadNext, + activeIndex, + selectionLength, +}) => ( + <> + + {activeIndex + 1} of {selectionLength} + + +); +SubmissionNavigation.defaultProps = { +}; +SubmissionNavigation.propTypes = { + hasPrevSubmission: PropTypes.bool.isRequired, + hasNextSubmission: PropTypes.bool.isRequired, + loadPrev: PropTypes.func.isRequired, + loadNext: PropTypes.func.isRequired, + activeIndex: PropTypes.number.isRequired, + selectionLength: PropTypes.number.isRequired, +}; + +export const mapStateToProps = (state) => ({ + hasPrevSubmission: selectors.grading.hasPrevSubmission(state), + hasNextSubmission: selectors.grading.hasNextSubmission(state), + activeIndex: selectors.grading.activeIndex(state), + selectionLength: selectors.grading.selectionLength(state), +}); + +export const mapDispatchToProps = { + loadPrev: thunkActions.grading.loadPrev, + loadNext: thunkActions.grading.loadNext, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(SubmissionNavigation); diff --git a/src/containers/ReviewModal/index.jsx b/src/containers/ReviewModal/index.jsx index 90e404d..8d717ef 100644 --- a/src/containers/ReviewModal/index.jsx +++ b/src/containers/ReviewModal/index.jsx @@ -4,16 +4,17 @@ import { connect } from 'react-redux'; import { FullscreenModal, - Container, + Row, + Col, } from '@edx/paragon'; -import createDOMPurify from 'dompurify'; -import parse from 'html-react-parser'; - import selectors from 'data/selectors'; import actions from 'data/actions'; import thunkActions from 'data/thunkActions'; +import ResponseDisplay from 'components/ResponseDisplay'; +import Rubric from 'containers/Rubric'; + import ReviewActions from './ReviewActions'; import './ReviewModal.scss'; @@ -24,16 +25,9 @@ import './ReviewModal.scss'; export class ReviewModal extends React.Component { constructor(props) { super(props); - console.log('review modal'); - console.log({ props }); - this.purify = createDOMPurify(window); this.onClose = this.onClose.bind(this); } - get textContent() { - return parse(this.purify.sanitize(this.props.response.text)); - } - onClose() { this.props.setShowReview(false); } @@ -48,10 +42,14 @@ export class ReviewModal extends React.Component { isOpen={this.props.isOpen} beforeBodyNode={} onClose={this.onClose} + className="review-modal" > - - {this.textContent} - +
+ + + { this.props.showRubric && } + +
); } @@ -66,12 +64,14 @@ ReviewModal.propTypes = { text: PropTypes.node, }), setShowReview: PropTypes.func.isRequired, + showRubric: PropTypes.bool.isRequired, }; export const mapStateToProps = (state) => ({ isOpen: selectors.app.showReview(state), oraName: selectors.app.oraName(state), response: selectors.grading.selected.response(state), + showRubric: selectors.app.showRubric(state), }); export const mapDispatchToProps = { diff --git a/src/containers/Rubric/GradingRubric.temp.jsx b/src/containers/Rubric/GradingRubric.temp.jsx new file mode 100644 index 0000000..3ff0a2a --- /dev/null +++ b/src/containers/Rubric/GradingRubric.temp.jsx @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { + Card, + Button, + Form, +} from '@edx/paragon'; + +/** + * + */ +export const GradingRubric = ({ +}) => { + return ( + + +

Rubric

+
+ + Which Color? + + Red + Green + Blue + Cyan + + + + + + + Which Color? + + Red + Green + Blue + Cyan + + + Make a selection + + + + + + Make a comment + + + + Which Color? + + Red + Green + Blue + Cyan + + + + + + +
+
+ +
+
+ ); +} + +GradingRubric.defaultProps = {}; +GradingRubric.propTypes = {}; + +export const mapStateToProps = (state) => ({ +}); + +export const mapDispatchToProps = { +}; + +export default connect(mapStateToProps, mapDispatchToProps)(GradingRubric); diff --git a/src/containers/Rubric/index.jsx b/src/containers/Rubric/index.jsx new file mode 100644 index 0000000..5a13a73 --- /dev/null +++ b/src/containers/Rubric/index.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { + Card, + Button, +} from '@edx/paragon'; + +/** + * + */ +export const Rubric = ({ +}) => { + return ( + + +
+ +
+
+ ); +} +Rubric.defaultProps = {}; +Rubric.propTypes = {}; + +export const mapStateToProps = (state) => ({ +}); + +export const mapDispatchToProps = { +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Rubric); diff --git a/src/data/actions/app.js b/src/data/actions/app.js index 1f05828..2bed0f7 100644 --- a/src/data/actions/app.js +++ b/src/data/actions/app.js @@ -8,10 +8,12 @@ export const loadCourseMetadata = createAction('loadCourseMetadata'); export const loadOraMetadata = createAction('loadOraMetadata'); export const setGrading = createAction('setGrading'); export const setShowReview = createAction('setReview'); +export const toggleShowRubric = createAction('toggleShowRubric'); export default StrictDict({ loadCourseMetadata, loadOraMetadata, setGrading, setShowReview, + toggleShowRubric, }); diff --git a/src/data/actions/grading.js b/src/data/actions/grading.js index e5b6f76..69599cc 100644 --- a/src/data/actions/grading.js +++ b/src/data/actions/grading.js @@ -10,7 +10,6 @@ const createAction = createActionFactory(dataKey); * @param {obj} submission data for the review/grading view * { * {obj} response - api response data - * {obj} staticData - api submission static data * {obj} gradeData - api grade data * {str} status - api grade status * } @@ -20,14 +19,14 @@ const loadSubmission = createAction('loadSubmission'); /** * Pre-load just the static info about the "next" submission in the review queue. * Load submission and the learner's response. - * @param {obj} submission ({ staticData, response }) + * @param {obj} submission ({ response }) */ const preloadNext = createAction('preloadNext'); /** * Pre-load just the static info about the "previous" submission in the review queue. * Load submission and the learner's response. - * @param {obj} submission ({ staticData, response }) + * @param {obj} submission ({ response }) */ const preloadPrev = createAction('preloadPrev'); diff --git a/src/data/reducers/app.js b/src/data/reducers/app.js index 3bfa1f0..fbd3893 100644 --- a/src/data/reducers/app.js +++ b/src/data/reducers/app.js @@ -13,6 +13,7 @@ const initialState = { org: '', }, showReview: false, + showRubric: false, grading: false, }; @@ -27,6 +28,8 @@ const app = (state = initialState, { type, payload }) => { return { ...state, showReview: payload }; case actions.app.setGrading.toString(): return { ...state, grading: payload }; + case actions.app.toggleShowRubric.toString(): + return { ...state, showRubric: !state.showRubric }; default: return state; } diff --git a/src/data/reducers/grading.js b/src/data/reducers/grading.js index 8cd861a..6468d6e 100644 --- a/src/data/reducers/grading.js +++ b/src/data/reducers/grading.js @@ -75,6 +75,7 @@ const app = (state = initialState, { type, payload }) => { ...state, prev: state.current, current: { + response: state.next.response, ...payload, }, activeIndex: state.activeIndex + 1, @@ -85,6 +86,7 @@ const app = (state = initialState, { type, payload }) => { ...state, next: state.current, current: { + response: state.prev.response, ...payload, }, activeIndex: state.activeIndex - 1, diff --git a/src/data/selectors/app.js b/src/data/selectors/app.js index f1f98ef..c6d22d7 100644 --- a/src/data/selectors/app.js +++ b/src/data/selectors/app.js @@ -2,6 +2,7 @@ import { StrictDict } from 'utils'; export const simpleSelectors = { showReview: state => state.app.showReview, + showRubric: state => state.app.showRubric, grading: state => state.app.grading, courseMetadata: state => state.app.courseMetadata, oraName: state => state.app.oraMetadata.name, diff --git a/src/data/selectors/grading.js b/src/data/selectors/grading.js index 741338b..f9e6d7b 100644 --- a/src/data/selectors/grading.js +++ b/src/data/selectors/grading.js @@ -10,6 +10,15 @@ export const simpleSelectors = { current: state => state.grading.current, }; +/** + * returns the length of the list of selected submissions + * @return {number} selected submission list length + */ +export const selectionLength = createSelector( + [module.simpleSelectors.selected], + (selected) => selected.length, +); + /** * returns the selected submission id * @return {string} selected submission id @@ -136,4 +145,6 @@ export default StrictDict({ nextSubmissionId, prevSubmissionId, selected, + selectedResponse, + selectionLength, }); diff --git a/src/data/selectors/submissions.js b/src/data/selectors/submissions.js index 3051fd5..7cc05b4 100644 --- a/src/data/selectors/submissions.js +++ b/src/data/selectors/submissions.js @@ -2,13 +2,9 @@ import _ from 'lodash'; import { createSelector } from 'reselect'; import { StrictDict } from 'utils'; -import * as module from './submissions'; export const simpleSelectors = { list: state => state.submissions.list, - selected: state => state.submissions.selected, - activeIndex: state => state.submissions.activeIndex, - current: state => state.submissions.current, }; /** @@ -22,131 +18,7 @@ export const listData = createSelector( ), ); -/** - * returns the selected submission id - * @return {string} selected submission id - */ -export const selectedSubmissionId = createSelector( - [module.simpleSelectors.selected, module.simpleSelectors.activeIndex], - (selected, index) => selected[index], -); - -/** - * returns static data from the active selected submission - * @return {obj} - staticData - * { submissionId, username, teamName, dateSubmitted } - */ -export const selectedStaticData = createSelector( - [module.selectedSubmissionId, module.simpleSelectors.list], - (submissionId, list) => { - const submission = list[submissionId]; - const { grade, gradeStatus, ...staticData } = submission; - return staticData; - }, -); - -/** - * Returns the username for the selected submission - * @return {string} username - */ -export const selectedUsername = createSelector( - [module.selectedStaticData], - (staticData) => staticData.username, -); - -/** - * Returns the grade status for the selected submission - * @return {string} grade status - */ -export const selectedGradeStatus = createSelector( - [module.simpleSelectors.current], - (current) => current.gradeStatus, -); - -/** - * Returns the grade data for the selected submission - * @return {obj} grade data - * { score, overallFeedback, criteria } - */ -export const selectedGradeData = createSelector( - [module.simpleSelectors.current], - (current) => current.gradeData, -); - -/** - * Returns the response data for the selected submission - * @return {obj} response - * { text, files: [] } - */ -export const selectedResponse = createSelector( - [module.simpleSelectors.current], - (current) => current.response, -); - -export const selected = StrictDict({ - submissionId: module.selectedSubmissionId, - staticData: module.selectedStaticData, - username: module.selectedUsername, - gradeStatus: module.selectedGradeStatus, - gradeData: module.selectedGradeData, - response: module.selectedResponse, -}); - -/** - * Returns true iff there exists a selection previous to the current selection - * in the queue. - * @return {bool} has previous submission? - */ -export const hasPrevSubmission = createSelector( - [simpleSelectors.activeIndex], - (activeIndex) => activeIndex > 0, -); - -/** - * Returns true iff there exists a selection after the current selection - * in the queue. - * @return {bool} has next submission? - */ -export const hasNextSubmission = createSelector( - [simpleSelectors.selected, simpleSelectors.activeIndex], - (list, activeIndex) => activeIndex < list.length - 1, -); - -/** - * Returns the submissionId for the previous submission in the selection queu - * @return {string} previous submission id (null if there isn't one) - */ -export const prevSubmissionId = createSelector( - [simpleSelectors.selected, simpleSelectors.activeIndex], - (list, activeIndex) => { - if (activeIndex > 0) { - return list[activeIndex - 1]; - } - return null; - }, -); - -/** - * Returns the submissionId for the next submission in the selection queu - * @return {string} next submission id (null if there isn't one) - */ -export const nextSubmissionId = createSelector( - [simpleSelectors.selected, simpleSelectors.activeIndex], - (list, activeIndex) => { - if (activeIndex < list.length - 1) { - return list[activeIndex + 1]; - } - return null; - }, -); - export default StrictDict({ ...simpleSelectors, listData, - selectedSubmissionId, - hasPrevSubmission, - hasNextSubmission, - nextSubmissionId, - prevSubmissionId, - selected, }); diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js index db97aec..9f0b73f 100644 --- a/src/data/services/lms/api.js +++ b/src/data/services/lms/api.js @@ -72,9 +72,9 @@ const fetchSubmission = mockSuccess((submissionId) => ( * }, * } */ -const fetchSubmissionStatus = mockSuccess((submissionId) => ({ - submissionData: fakeData.mockSubmission(submissionId), -})); +const fetchSubmissionStatus = mockSuccess((submissionId) => ( + fakeData.mockSubmissionStatus(submissionId) +)); /** * Fetches only the learner response for a given submission. Used for pre-fetching response diff --git a/src/data/services/lms/fakeData/index.js b/src/data/services/lms/fakeData/index.js index 16705fa..ba8d002 100644 --- a/src/data/services/lms/fakeData/index.js +++ b/src/data/services/lms/fakeData/index.js @@ -1,5 +1,5 @@ import submissions from './submissionList'; -import { mockSubmission } from './submissionFull'; +import { mockSubmission, mockSubmissionStatus } from './submissionFull'; import oraMetadata from './ora'; import courseMetadata from './course'; import ids from './ids'; @@ -9,5 +9,6 @@ export default { oraMetadata, courseMetadata, mockSubmission, + mockSubmissionStatus, ids, }; diff --git a/src/data/services/lms/fakeData/ora.js b/src/data/services/lms/fakeData/ora.js index 13f7609..b5b502b 100644 --- a/src/data/services/lms/fakeData/ora.js +++ b/src/data/services/lms/fakeData/ora.js @@ -15,7 +15,7 @@ const rubricConfig = { comments: 'rubric-level comments', criteria: [ { - name: 'First Criterion', + name: 'firstCriterion', orderNum: 0, prompt: 'A criterion prompt', feedback: 'optional', diff --git a/src/data/services/lms/fakeData/submissionFull.js b/src/data/services/lms/fakeData/submissionFull.js index 29316f4..c0fb837 100644 --- a/src/data/services/lms/fakeData/submissionFull.js +++ b/src/data/services/lms/fakeData/submissionFull.js @@ -1,21 +1,22 @@ import submissionList from './submissionList'; -const responseText = `

Title

+const responseText = (submissionId) => `

Title (${submissionId})

Phasellus tempor eros aliquam ipsum molestie, vitae varius lectus tempus. Morbi iaculis, libero euismod vehicula rutrum, nisi leo volutpat diam, quis commodo ex nunc ut odio. Pellentesque condimentum feugiat erat ac vulputate. Pellentesque porta rutrum sagittis. Curabitur vulputate tempus accumsan. Fusce bibendum gravida metus a scelerisque. Mauris fringilla orci non lobortis commodo. Quisque iaculis, quam a tincidunt vehicula, erat nisi accumsan quam, eu cursus ligula magna id odio. Nulla porttitor, lorem gravida vehicula tristique, sapien metus tristique ex, id tincidunt sapien justo nec sapien. Maecenas luctus, nisl vestibulum scelerisque pharetra, ligula orci vulputate turpis, in ultrices mauris dolor eu enim. Suspendisse quis nibh nec augue semper maximus. Morbi maximus eleifend magna. Phasellus porttitor vel magna et auctor. Nulla porttitor convallis aliquam. Donec cursus, ipsum ut egestas bibendum, purus metus dignissim est, ac condimentum leo felis eget diam. In magna mi, tincidunt id sapien id, fermentum vestibulum quam. Quisque et dui sed urna convallis rutrum pellentesque quis sapien. Cras non lectus velit. Praesent semper eros id risus mollis, quis interdum quam imperdiet. Sed nec vulputate tortor, at tristique tortor.
`; // eslint-disable-next-line -export const mockSubmission = (submissionId) => { - console.log({ submissionId, submission: submissionList[submissionId] }); - return { - response: { - text: responseText, - files: [], - }, - gradeStatus: submissionList[submissionId].gradeStatus, - gradeData: submissionList[submissionId].grade, - }; -}; +export const mockSubmission = (submissionId) => ({ + response: { + text: responseText(submissionId), + files: [], + }, + gradeStatus: submissionList[submissionId].gradeStatus, + score: submissionList[submissionId].score, +}); +export const mockSubmissionStatus = (submissionId) => ({ + gradeData: submissionList[submissionId].gradeData, + gradeStatus: submissionList[submissionId].gradeStatus, +}); diff --git a/src/data/services/lms/fakeData/submissionList.js b/src/data/services/lms/fakeData/submissionList.js index 52963d0..b43a291 100644 --- a/src/data/services/lms/fakeData/submissionList.js +++ b/src/data/services/lms/fakeData/submissionList.js @@ -22,17 +22,27 @@ const day = 86400000; const submissions = {}; let lastIndex = 0; -const createSubmission = (grade, gradeStatus) => { +const createSubmission = (score, gradeStatus) => { const index = lastIndex; lastIndex += 1; const submissionId = ids.submissionId(index); + const gradeData = score === null ? null : { + score, + overallFeedback: 'was okay', + criteria: [{ + name: 'firstCriterion', + feedback: 'did alright', + selectedOption: 'good' + }], + }; submissions[submissionId] = { submissionId, username: ids.username(index), // teamName: '', dateSubmitted: date0 + (day * index), + score, + gradeData, gradeStatus, - grade, }; }; diff --git a/src/data/thunkActions/grading.js b/src/data/thunkActions/grading.js index 9a1025c..61f8d32 100644 --- a/src/data/thunkActions/grading.js +++ b/src/data/thunkActions/grading.js @@ -3,6 +3,7 @@ import { StrictDict } from 'utils'; import actions from 'data/actions'; import selectors from 'data/selectors'; import api from 'data/services/lms/api'; +import * as module from './grading'; /** * Prefetch the "next" submission in the selected queue. Only fetches the response info. @@ -11,7 +12,7 @@ export const prefetchNext = () => (dispatch, getState) => ( api.fetchSubmissionResponse( selectors.grading.nextSubmissionId(getState()), ).then((response) => { - dispatch(actions.grading.preloadNext(response.submission)); + dispatch(actions.grading.preloadNext(response)); }) ); @@ -22,7 +23,7 @@ export const prefetchPrev = () => (dispatch, getState) => ( api.fetchSubmissionResponse( selectors.grading.prevSubmissionId(getState()), ).then((response) => { - dispatch(actions.grading.preloadPrev(response.submissionStatus)); + dispatch(actions.grading.preloadPrev(response)); }) );