@pie-lib/rubric 1.0.0-beta.5 → 1.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/authoring.jsx CHANGED
@@ -1,26 +1,27 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { withStyles } from '@material-ui/core/styles';
4
- import classNames from 'classnames';
5
- import OutlinedInput from '@material-ui/core/OutlinedInput';
6
- import InputLabel from '@material-ui/core/InputLabel';
7
- import Select from '@material-ui/core/Select';
8
- import FormControl from '@material-ui/core/FormControl';
9
- import MenuItem from '@material-ui/core/MenuItem';
3
+ import { styled } from '@mui/material/styles';
4
+
5
+ import InputLabel from '@mui/material/InputLabel';
6
+ import OutlinedInput from '@mui/material/OutlinedInput';
7
+ import Select from '@mui/material/Select';
8
+ import FormControl from '@mui/material/FormControl';
9
+ import MenuItem from '@mui/material/MenuItem';
10
10
  import times from 'lodash/times';
11
- import Checkbox from '@material-ui/core/Checkbox';
12
- import FormGroup from '@material-ui/core/FormGroup';
13
- import FormControlLabel from '@material-ui/core/FormControlLabel';
14
- import grey from '@material-ui/core/colors/grey';
15
- import Typography from '@material-ui/core/Typography';
16
- import DragIndicator from '@material-ui/icons/DragIndicator';
17
- import EditableHtml from '@pie-lib/editable-html';
18
- import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
11
+ import Checkbox from '@mui/material/Checkbox';
12
+ import FormGroup from '@mui/material/FormGroup';
13
+ import FormControlLabel from '@mui/material/FormControlLabel';
14
+ import Typography from '@mui/material/Typography';
15
+ import DragIndicator from '@mui/icons-material/DragIndicator';
16
+ import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
19
17
  import debug from 'debug';
20
18
  import takeRight from 'lodash/takeRight';
21
19
  import PointMenu from './point-menu';
22
-
23
20
  import range from 'lodash/range';
21
+ import EditableHtml from '@pie-lib/editable-html';
22
+ import { InputContainer } from '@pie-lib/config-ui';
23
+ import { grey } from '@mui/material/colors';
24
+
24
25
  const log = debug('pie-lib:rubric:authoring');
25
26
 
26
27
  const reorder = (list, startIndex, endIndex) => {
@@ -37,23 +38,25 @@ export const RubricType = PropTypes.shape({
37
38
  points: PropTypes.arrayOf(PropTypes.string),
38
39
  sampleAnswers: PropTypes.arrayOf(PropTypes.string),
39
40
  maxPoints: PropTypes.number,
41
+ rubriclessInstruction: PropTypes.string,
40
42
  });
41
43
 
42
- const MaxPoints = withStyles((theme) => ({
43
- formControl: {
44
- minWidth: '120px',
45
- margin: theme.spacing.unit,
46
- },
47
- }))((props) => {
48
- const { value, onChange, max, classes } = props;
44
+ const MaxPoints = (props) => {
45
+ const { value, onChange, max } = props;
46
+ const labelId = 'max-points-label';
49
47
 
50
48
  return (
51
- <FormControl className={classes.formControl} variant="outlined">
52
- <InputLabel width={100} htmlFor="...">
53
- Max Points
54
- </InputLabel>
55
- <Select value={value} onChange={(e) => onChange(e.target.value)} input={<OutlinedInput labelWidth={80} />}>
56
- {range(1, max).map((v) => (
49
+ <FormControl sx={{ minWidth: 120, m: 1 }} variant="outlined">
50
+ <InputLabel id={labelId}>Max Points</InputLabel>
51
+ <Select
52
+ labelId={labelId}
53
+ label="Max Points"
54
+ value={value}
55
+ onChange={(e) => onChange(e.target.value)}
56
+ input={<OutlinedInput label="Max Points" />}
57
+ MenuProps={{ transitionDuration: { enter: 225, exit: 195 } }}
58
+ >
59
+ {range(1, max + 1).map((v) => (
57
60
  <MenuItem key={`${v}`} value={v}>
58
61
  {v}
59
62
  </MenuItem>
@@ -61,89 +64,102 @@ const MaxPoints = withStyles((theme) => ({
61
64
  </Select>
62
65
  </FormControl>
63
66
  );
64
- });
67
+ };
65
68
 
66
69
  // if the value is null or 'null', the Sample Answer input field for that point will not be dispalyed
67
70
  // if the value is '', the Sample Answer input field will be empty
68
71
  const checkSampleAnswer = (sampleAnswer) => sampleAnswer === null || sampleAnswer === 'null';
69
72
 
70
- export const PointConfig = withStyles((theme) => ({
71
- pointConfig: {},
72
- row: {
73
- display: 'flex',
74
- width: '100%',
75
- position: 'relative',
76
- },
77
- editor: {
78
- width: '100%',
79
- backgroundColor: `${theme.palette.common.white} !important`,
80
- },
81
- dragIndicator: {
82
- paddingTop: theme.spacing.unit,
83
- color: grey[500],
84
- },
85
- pointsLabel: {
86
- color: grey[500],
87
- paddingBottom: theme.spacing.unit,
88
- textTransform: 'uppercase',
89
- },
90
- sampleAnswersEditor: {
91
- paddingLeft: theme.spacing.unit * 3,
92
- },
93
- pointMenu: {
94
- position: 'absolute',
95
- right: 0,
96
- },
97
- }))((props) => {
98
- const { points, content, classes, sampleAnswer, mathMlOptions = {} } = props;
73
+ const Row = styled('div')(() => ({ display: 'flex', width: '100%', position: 'relative' }));
74
+
75
+ const EditorDiv = styled('div')(({ theme }) => ({ width: '100%', backgroundColor: `${theme.palette.common.white}` }));
76
+
77
+ const DragIndicatorStyled = styled(DragIndicator)(({ theme }) => ({ paddingTop: theme.spacing(1), color: grey[500] }));
78
+
79
+ const PointsLabel = styled(Typography)(({ theme }) => ({
80
+ color: grey[500],
81
+ paddingBottom: theme.spacing(1),
82
+ textTransform: 'uppercase',
83
+ }));
84
+
85
+ const SampleAnswersEditor = styled('div')(({ theme }) => ({ paddingLeft: theme.spacing(3) }));
86
+
87
+ const ErrorText = styled('div')(({ theme }) => ({
88
+ fontSize: theme.typography.fontSize - 2,
89
+ color: theme.palette.error.main,
90
+ paddingLeft: theme.spacing(3),
91
+ paddingTop: theme.spacing(1),
92
+ }));
93
+
94
+ const PointMenuWrapper = styled('div')(() => ({ position: 'absolute', right: 0 }));
95
+
96
+ export const PointConfig = (props) => {
97
+ const { points, content, sampleAnswer, mathMlOptions = {}, error, pluginOpts = {} } = props;
99
98
  const pointsLabel = `${points} ${points <= 1 ? 'pt' : 'pts'}`;
100
99
  const showSampleAnswer = checkSampleAnswer(sampleAnswer);
101
100
 
102
101
  return (
103
- <div className={classes.pointConfig}>
104
- <Typography variant="overline" className={classes.pointsLabel}>
105
- {pointsLabel}
106
- </Typography>
107
-
108
- <div className={classes.row}>
109
- <DragIndicator className={classes.dragIndicator} />
110
- <EditableHtml
111
- className={classes.editor}
112
- markup={content}
113
- onChange={props.onChange}
114
- mathMlOptions={mathMlOptions}
115
- />
116
- <PointMenu
117
- classes={{
118
- icon: classes.pointMenu,
119
- }}
120
- showSampleAnswer={showSampleAnswer}
121
- onChange={props.onMenuChange}
122
- />
123
- </div>
124
-
125
- {!showSampleAnswer && (
126
- <div className={classes.sampleAnswersEditor}>
127
- <Typography variant="overline" className={classes.dragIndicator}>
128
- Sample Response
129
- </Typography>
102
+ <div>
103
+ <PointsLabel variant="overline">{pointsLabel}</PointsLabel>
104
+ <Row>
105
+ <DragIndicatorStyled />
106
+ <EditorDiv>
130
107
  <EditableHtml
131
- className={classes.editor}
132
- markup={sampleAnswer}
133
- onChange={props.onSampleChange}
108
+ error={error}
109
+ pluginProps={pluginOpts}
110
+ markup={content}
111
+ onChange={props.onChange}
134
112
  mathMlOptions={mathMlOptions}
135
113
  />
136
- </div>
114
+ </EditorDiv>
115
+ <PointMenuWrapper>
116
+ <PointMenu showSampleAnswer={showSampleAnswer} onChange={props.onMenuChange} />
117
+ </PointMenuWrapper>
118
+ </Row>
119
+ {error && <ErrorText>{error}</ErrorText>}
120
+ {!showSampleAnswer && (
121
+ <SampleAnswersEditor>
122
+ <DragIndicatorStyled as={Typography} variant="overline">
123
+ Sample Response
124
+ </DragIndicatorStyled>
125
+ <EditorDiv>
126
+ <EditableHtml
127
+ markup={sampleAnswer}
128
+ pluginProps={pluginOpts}
129
+ onChange={props.onSampleChange}
130
+ mathMlOptions={mathMlOptions}
131
+ />
132
+ </EditorDiv>
133
+ </SampleAnswersEditor>
137
134
  )}
138
135
  </div>
139
136
  );
140
- });
137
+ };
138
+
139
+ const Container = styled('div')(({ theme }) => ({
140
+ backgroundColor: grey[200],
141
+ borderWidth: 1,
142
+ borderStyle: 'solid',
143
+ borderColor: grey[300],
144
+ padding: theme.spacing(2),
145
+ margin: theme.spacing(1),
146
+ }));
147
+ const InputContainerWrapper = styled('div')(({ theme }) => ({
148
+ width: '100%',
149
+ paddingTop: theme.spacing(2),
150
+ marginBottom: theme.spacing(2),
151
+ '& MuiFormControl-root': { width: '100%' },
152
+ }));
153
+ const Rubricless = styled('div')(() => ({ display: 'none' }));
154
+ const ConfigHolder = styled('div')(({ theme }) => ({ paddingTop: theme.spacing(1), paddingBottom: theme.spacing(1) }));
155
+ const RubricTitle = styled(Typography)(({ theme }) => ({ paddingLeft: theme.spacing(1), margin: theme.spacing(1) }));
141
156
 
142
157
  export class RawAuthoring extends React.Component {
143
158
  static propTypes = {
144
- classes: PropTypes.object.isRequired,
145
- className: PropTypes.string,
146
159
  value: RubricType,
160
+ config: PropTypes.object,
161
+ pluginOpts: PropTypes.object,
162
+ rubricless: PropTypes.bool,
147
163
  onChange: PropTypes.func,
148
164
  };
149
165
 
@@ -162,8 +178,13 @@ export class RawAuthoring extends React.Component {
162
178
  onChange({ ...value, points, sampleAnswers });
163
179
  };
164
180
 
165
- changeMaxPoints = (maxPoints) => {
181
+ changeRubriclessInstruction = (input) => {
166
182
  const { value, onChange } = this.props;
183
+ onChange({ ...value, rubriclessInstruction: input });
184
+ };
185
+
186
+ changeMaxPoints = (maxPoints) => {
187
+ const { value, onChange, rubricless } = this.props;
167
188
  const currentMax = value.points.length - 1;
168
189
 
169
190
  log('current', currentMax, 'new: ', maxPoints);
@@ -184,8 +205,10 @@ export class RawAuthoring extends React.Component {
184
205
  sampleAnswers = takeRight(value.sampleAnswers, maxPoints + 1);
185
206
  }
186
207
 
187
- if (points) {
188
- onChange({ ...value, points, sampleAnswers });
208
+ if (points && !rubricless) {
209
+ onChange({ ...value, points, sampleAnswers, maxPoints });
210
+ } else {
211
+ onChange({ ...value, maxPoints });
189
212
  }
190
213
  };
191
214
 
@@ -242,21 +265,37 @@ export class RawAuthoring extends React.Component {
242
265
  };
243
266
 
244
267
  render() {
245
- const { classes, className, value, mathMlOptions = {} } = this.props;
246
- let { excludeZeroEnabled = true, maxPointsEnabled = true } = value || {};
247
-
268
+ const { value, mathMlOptions = {}, config = {}, rubricless = false, pluginOpts = {} } = this.props;
269
+ let {
270
+ excludeZeroEnabled = true,
271
+ maxPointsEnabled = true,
272
+ errors = {},
273
+ rubriclessInstructionEnabled = false,
274
+ maxPoints = 10,
275
+ } = value || {};
276
+ // rubric will contain a max value for maxPoints
277
+ const { rubriclessInstruction = {}, maxMaxPoints = 10 } = config || {};
278
+ const { pointsDescriptorsErrors } = errors || {};
248
279
  if (value && Number.isFinite(value.maxPoints)) {
249
280
  // eslint-disable-next-line no-console
250
281
  console.warn('maxPoints is deprecated - remove from model');
251
282
  }
252
283
 
284
+ // for rubric value is computed based on points
285
+ const maxPointsValue = !rubricless ? value.points.length - 1 : maxPoints;
286
+
253
287
  return (
254
- <div className={classNames(classes.class, className)}>
255
- <Typography variant="h5" className={classes.rubricTitle}>
256
- Rubric
257
- </Typography>
288
+ <div>
289
+ <RubricTitle variant="h5">Rubric</RubricTitle>
258
290
  <FormGroup row>
259
- {maxPointsEnabled && <MaxPoints max={10} value={value.points.length - 1} onChange={this.changeMaxPoints} />}
291
+ {maxPointsEnabled && (
292
+ <MaxPoints
293
+ max={maxMaxPoints < 100 ? maxMaxPoints : 100}
294
+ value={maxPointsValue}
295
+ onChange={this.changeMaxPoints}
296
+ pluginOpts={pluginOpts}
297
+ />
298
+ )}
260
299
  {excludeZeroEnabled && (
261
300
  <FormControlLabel
262
301
  label="Exclude zeros"
@@ -265,69 +304,77 @@ export class RawAuthoring extends React.Component {
265
304
  )}
266
305
  </FormGroup>
267
306
 
268
- <div className={classes.container}>
269
- <DragDropContext onDragEnd={this.dragEnd}>
270
- <Droppable droppableId="droppable">
271
- {(provided) => (
272
- <div {...provided.droppableProps} ref={provided.innerRef}>
273
- {value.points.map(
274
- (p, index) =>
275
- this.shouldRenderPoint(index, value) && (
276
- <Draggable key={`${p.points}-${index}`} index={index} draggableId={index.toString()}>
277
- {(provided) => (
278
- <div
279
- className={classes.configHolder}
280
- ref={provided.innerRef}
281
- {...provided.draggableProps}
282
- {...provided.dragHandleProps}
283
- >
284
- <PointConfig
285
- points={value.points.length - 1 - index}
286
- content={p}
287
- sampleAnswer={value.sampleAnswers && value.sampleAnswers[index]}
288
- onChange={(content) => this.changeContent(index, content, 'points')}
289
- onSampleChange={(content) => this.changeContent(index, content, 'sampleAnswers')}
290
- onMenuChange={(clickedItem) => this.onPointMenuChange(index, clickedItem)}
291
- mathMlOptions={mathMlOptions}
292
- />
293
- </div>
294
- )}
295
- </Draggable>
296
- ),
307
+ {rubriclessInstructionEnabled && rubricless && (
308
+ <InputContainerWrapper>
309
+ <InputContainer label={rubriclessInstruction.label}>
310
+ <EditableHtml
311
+ markup={value.rubriclessInstruction || ''}
312
+ onChange={this.changeRubriclessInstruction}
313
+ pluginProps={pluginOpts}
314
+ nonEmpty={false}
315
+ disableUnderline
316
+ languageCharactersProps={[{ language: 'spanish' }, { language: 'special' }]}
317
+ mathMlOptions={mathMlOptions}
318
+ />
319
+ </InputContainer>
320
+ </InputContainerWrapper>
321
+ )}
322
+
323
+ <div>
324
+ {rubricless ? (
325
+ <Rubricless />
326
+ ) : (
327
+ <Container>
328
+ <DragDropContext onDragEnd={this.dragEnd}>
329
+ <Droppable droppableId="droppable">
330
+ {(provided) => (
331
+ <div {...provided.droppableProps} ref={provided.innerRef}>
332
+ {value.points.map(
333
+ (p, index) =>
334
+ this.shouldRenderPoint(index, value) && (
335
+ <Draggable key={`${p.points}-${index}`} index={index} draggableId={index.toString()}>
336
+ {(provided) => (
337
+ <ConfigHolder
338
+ ref={provided.innerRef}
339
+ {...provided.draggableProps}
340
+ {...provided.dragHandleProps}
341
+ >
342
+ <PointConfig
343
+ points={value.points.length - 1 - index}
344
+ content={p}
345
+ error={
346
+ pointsDescriptorsErrors &&
347
+ pointsDescriptorsErrors[value.points.length - 1 - index]
348
+ }
349
+ sampleAnswer={value.sampleAnswers && value.sampleAnswers[index]}
350
+ onChange={(content) => this.changeContent(index, content, 'points')}
351
+ onSampleChange={(content) => this.changeContent(index, content, 'sampleAnswers')}
352
+ onMenuChange={(clickedItem) => this.onPointMenuChange(index, clickedItem)}
353
+ mathMlOptions={mathMlOptions}
354
+ pluginOpts={pluginOpts}
355
+ />
356
+ </ConfigHolder>
357
+ )}
358
+ </Draggable>
359
+ ),
360
+ )}
361
+ {provided.placeholder}
362
+ </div>
297
363
  )}
298
- {provided.placeholder}
299
- </div>
300
- )}
301
- </Droppable>
302
- </DragDropContext>
364
+ </Droppable>
365
+ </DragDropContext>
366
+ </Container>
367
+ )}
303
368
  </div>
304
369
  </div>
305
370
  );
306
371
  }
307
372
  }
308
373
 
309
- const styles = (theme) => ({
310
- container: {
311
- backgroundColor: grey[200],
312
- borderWidth: 1,
313
- borderStyle: 'solid',
314
- borderColor: grey[300],
315
- padding: theme.spacing.unit * 2,
316
- margin: theme.spacing.unit,
317
- },
318
- configHolder: {
319
- paddingTop: theme.spacing.unit,
320
- paddingBottom: theme.spacing.unit,
321
- },
322
- rubricTitle: {
323
- paddingLeft: theme.spacing.unit,
324
- margin: theme.spacing.unit,
325
- },
326
- });
327
-
328
- const StyledRawAuthoring = withStyles(styles)(RawAuthoring);
374
+ // styles migrated to styled-components above
329
375
 
330
376
  const Reverse = (props) => {
377
+ const { rubricless = false, config = {}, pluginOpts = {} } = props || {};
331
378
  const points = Array.from(props.value.points || []).reverse();
332
379
  let sampleAnswers = Array.from(props.value.sampleAnswers || []).reverse();
333
380
 
@@ -347,11 +394,16 @@ const Reverse = (props) => {
347
394
  });
348
395
  };
349
396
 
350
- return <StyledRawAuthoring value={value} onChange={onChange} />;
397
+ return (
398
+ <RawAuthoring value={value} config={config} onChange={onChange} rubricless={rubricless} pluginOpts={pluginOpts} />
399
+ );
351
400
  };
352
401
 
353
402
  Reverse.propTypes = {
354
403
  value: RubricType,
404
+ config: PropTypes.object,
405
+ pluginOpts: PropTypes.object,
406
+ rubricless: PropTypes.bool,
355
407
  getIndex: PropTypes.func,
356
408
  onChange: PropTypes.func,
357
409
  };
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ import Authoring from './authoring';
3
3
  const RUBRIC_TYPES = {
4
4
  SIMPLE_RUBRIC: 'simpleRubric',
5
5
  MULTI_TRAIT_RUBRIC: 'multiTraitRubric',
6
+ RUBRICLESS: 'rubricless',
6
7
  };
7
8
 
8
9
  export { Authoring, RUBRIC_TYPES };
@@ -1,8 +1,8 @@
1
- import Menu from '@material-ui/core/Menu';
2
- import MenuItem from '@material-ui/core/MenuItem';
3
- import MoreVertIcon from '@material-ui/icons/MoreVert';
4
- import MoreHorizIcon from '@material-ui/icons/MoreHoriz';
5
- import IconButton from '@material-ui/core/IconButton';
1
+ import Menu from '@mui/material/Menu';
2
+ import MenuItem from '@mui/material/MenuItem';
3
+ import MoreVertIcon from '@mui/icons-material/MoreVert';
4
+ import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
5
+ import IconButton from '@mui/material/IconButton';
6
6
  import PropTypes from 'prop-types';
7
7
  import React from 'react';
8
8
 
@@ -10,7 +10,6 @@ export class IconMenu extends React.Component {
10
10
  static propTypes = {
11
11
  opts: PropTypes.object,
12
12
  onClick: PropTypes.func.isRequired,
13
- classes: PropTypes.object.isRequired,
14
13
  };
15
14
 
16
15
  constructor(props) {
@@ -26,7 +25,7 @@ export class IconMenu extends React.Component {
26
25
  handleRequestClose = () => this.setState({ open: false });
27
26
 
28
27
  render() {
29
- const { opts, onClick, classes } = this.props;
28
+ const { opts, onClick } = this.props;
30
29
  const { open, anchorEl } = this.state;
31
30
  const keys = Object.keys(opts) || [];
32
31
 
@@ -40,7 +39,7 @@ export class IconMenu extends React.Component {
40
39
  return (
41
40
  <div>
42
41
  <div onClick={this.handleClick}>
43
- <IconButton className={classes.icon}>
42
+ <IconButton size="large">
44
43
  {open ? <MoreVertIcon color={iconColor} /> : <MoreHorizIcon color={iconColor} />}
45
44
  </IconButton>
46
45
  </div>
@@ -69,16 +68,11 @@ export class IconMenu extends React.Component {
69
68
  export default class PointMenu extends React.Component {
70
69
  static propTypes = {
71
70
  onChange: PropTypes.func.isRequired,
72
- classes: PropTypes.object.isRequired,
73
71
  showSampleAnswer: PropTypes.bool.isRequired,
74
72
  };
75
73
 
76
- static defaultProps = {
77
- classes: {},
78
- };
79
-
80
74
  render() {
81
- const { onChange, classes, showSampleAnswer } = this.props;
75
+ const { onChange, showSampleAnswer } = this.props;
82
76
  const sampleText = showSampleAnswer ? 'Provide Sample Response' : 'Remove Sample Response';
83
77
 
84
78
  return (
@@ -87,7 +81,6 @@ export default class PointMenu extends React.Component {
87
81
  opts={{
88
82
  sample: sampleText,
89
83
  }}
90
- classes={classes}
91
84
  />
92
85
  );
93
86
  }