@nualang/nualang-ui-components 0.1.1400 → 0.1.1402

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.
Files changed (32) hide show
  1. package/dist/Exercises/Roleplay/Games/TopBarActions.js +10 -15
  2. package/dist/Exercises/Roleplay/OriginalRoleplay.js +11 -16
  3. package/dist/Forms/Settings/Tabs/General/General.js +67 -27
  4. package/dist/Lists/ExerciseSelection/ExerciseSelection.js +7 -3
  5. package/dist/Lists/Exercises/Exercises.js +4 -7
  6. package/dist/Lists/Members/Members.js +57 -12
  7. package/dist/Lists/Members/Members.test.js +70 -9
  8. package/dist/Lists/PhraseListSelection/PhraseListSelection.js +8 -4
  9. package/dist/Lists/RoleplaySelection/RoleplaySelection.js +8 -4
  10. package/dist/Misc/AchievementsBanner/AchievementsBanner.js +62 -0
  11. package/dist/Misc/ColorLinearProgress/ColorLinearProgress.js +14 -17
  12. package/dist/Misc/ExerciseProgressBar/ExerciseProgressBar.js +10 -16
  13. package/dist/Misc/MedalCard/MedalCard.js +98 -0
  14. package/dist/Misc/MedalSelector/MedalSelector.js +4 -0
  15. package/dist/Misc/MedalSelector/PlaceholderMedal.js +96 -0
  16. package/dist/Misc/MedalSelector/PlaceholderMedal.svg +27 -0
  17. package/dist/Misc/NualaMascot/NualaMascot.js +68 -0
  18. package/dist/Misc/NualaMascot/NualaMascotGreen.js +212 -0
  19. package/dist/Misc/NualaMascot/NualaMascotPink.js +131 -0
  20. package/dist/Misc/NualaMascot/NualaMascotRed.js +148 -0
  21. package/dist/Misc/NualaMascot/NualaMascotYellow.js +140 -0
  22. package/dist/Navigation/ResponsiveDrawer/ResponsiveDrawer.js +5 -4
  23. package/dist/Tables/Progress/Progress.js +17 -3
  24. package/dist/Tables/Progress/ProgressTable.js +4 -1
  25. package/dist/Tables/Progress/ProgressTable.test.js +24 -0
  26. package/dist/Tables/Progress/TableToolbar.js +25 -1
  27. package/dist/Tables/Progress/TableToolbar.test.js +62 -0
  28. package/dist/Tables/Progress/sorting.js +3 -2
  29. package/dist/Tables/Progress/sorting.test.js +36 -0
  30. package/dist/index.js +4 -1
  31. package/dist/utils/studentSort.js +35 -0
  32. package/package.json +4 -4
@@ -7,23 +7,10 @@ import EditIcon from "@mui/icons-material/Create";
7
7
  import Share from "@mui/icons-material/Share";
8
8
  import MoreVertIcon from "@mui/icons-material/MoreVert";
9
9
  import { makeStyles } from "tss-react/mui";
10
- import { withStyles } from "tss-react/mui";
11
10
  import { alpha } from "@mui/material/styles";
12
11
  import VoiceSpeedSelector from "../../../Misc/VoiceSpeed/VoiceSpeed";
13
12
  import ReportProblem from "../../../Misc/ReportProblem/ReportProblem";
14
13
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
15
- const RoundedLinearProgress = withStyles(LinearProgress, theme => ({
16
- root: {
17
- height: 12,
18
- borderRadius: 4
19
- },
20
- colorPrimary: {
21
- backgroundColor: alpha(theme.palette.success.light, 0.25)
22
- },
23
- barColorPrimary: {
24
- backgroundColor: theme.palette.success.main
25
- }
26
- }));
27
14
  const useStyles = makeStyles()((theme, _params, classes) => ({
28
15
  label: {
29
16
  color: "#fff"
@@ -238,13 +225,21 @@ const TopBarActions = ({
238
225
  children: roleplayHasStarted || isRoleplayPreview ? /*#__PURE__*/_jsxs(_Fragment, {
239
226
  children: [!isRoleplayPreview && /*#__PURE__*/_jsx("div", {
240
227
  className: classes.progress,
241
- children: /*#__PURE__*/_jsx(RoundedLinearProgress, {
228
+ children: /*#__PURE__*/_jsx(LinearProgress, {
242
229
  variant: "determinate",
243
230
  value: currentMessages.length / roleplay.script.length * 100,
244
231
  "aria-label": t("exercise"),
245
232
  "aria-valuemin": 0,
246
233
  "aria-valuemax": 100,
247
- "aria-valuenow": currentMessages.length / roleplay.script.length * 100
234
+ "aria-valuenow": currentMessages.length / roleplay.script.length * 100,
235
+ sx: {
236
+ height: 12,
237
+ borderRadius: 4,
238
+ backgroundColor: theme => alpha(theme.palette.success.light, 0.25),
239
+ "& .MuiLinearProgress-bar": {
240
+ backgroundColor: theme => theme.palette.success.main
241
+ }
242
+ }
248
243
  })
249
244
  }), hiddenDown ? null : /*#__PURE__*/_jsxs(_Fragment, {
250
245
  children: [/*#__PURE__*/_jsx(VoiceSpeedSelector, {
@@ -1,9 +1,8 @@
1
1
  import { useEffect, useState, memo } from "react";
2
2
  import { Joyride, STATUS } from "react-joyride";
3
3
  import { useTheme, alpha } from "@mui/material/styles";
4
- import { withStyles } from "tss-react/mui";
5
4
  import { makeStyles } from "tss-react/mui";
6
- import { Grid, Slide, IconButton, Button, ButtonBase, Avatar, Dialog, Typography, Box, Divider, Tooltip, LinearProgress, useMediaQuery } from "@mui/material";
5
+ import { Grid, Slide, IconButton, Button, ButtonBase, Avatar, Dialog, Typography, Box, Divider, Tooltip, useMediaQuery, LinearProgress } from "@mui/material";
7
6
  import Alert from "@mui/material/Alert";
8
7
  import Menu from "@mui/material/Menu";
9
8
  import MenuItem from "@mui/material/MenuItem";
@@ -31,18 +30,6 @@ import ReportProblem from "../../Misc/ReportProblem/ReportProblem";
31
30
  import { removeExtraWhiteSpaces } from "../../utils";
32
31
  import DefaultButton from "../../Misc/DefaultColourButton/DefaultColourButton";
33
32
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
34
- const RoundedLinearProgress = withStyles(LinearProgress, theme => ({
35
- root: {
36
- height: 12,
37
- borderRadius: 4
38
- },
39
- colorPrimary: {
40
- backgroundColor: alpha(theme.palette.success.light, 0.25)
41
- },
42
- barColorPrimary: {
43
- backgroundColor: theme.palette.success.main
44
- }
45
- }));
46
33
  const useStyles = makeStyles()((theme, {
47
34
  isActorOne
48
35
  }, classes) => ({
@@ -629,9 +616,17 @@ function Roleplay({
629
616
  children: roleplayHasStarted ? /*#__PURE__*/_jsxs(_Fragment, {
630
617
  children: [/*#__PURE__*/_jsx("div", {
631
618
  className: classes.progress,
632
- children: /*#__PURE__*/_jsx(RoundedLinearProgress, {
619
+ children: /*#__PURE__*/_jsx(LinearProgress, {
633
620
  variant: "determinate",
634
- value: currentMessages.length / roleplay.script.length * 100
621
+ value: currentMessages.length / roleplay.script.length * 100,
622
+ sx: {
623
+ height: 12,
624
+ borderRadius: 4,
625
+ backgroundColor: theme => alpha(theme.palette.success.light, 0.25),
626
+ "& .MuiLinearProgress-bar": {
627
+ backgroundColor: theme => theme.palette.success.main
628
+ }
629
+ }
635
630
  })
636
631
  }), hiddenDown ? null : /*#__PURE__*/_jsxs(_Fragment, {
637
632
  children: [/*#__PURE__*/_jsxs(Button, {
@@ -56,7 +56,8 @@ function GeneralForm({
56
56
  }) {
57
57
  const {
58
58
  email,
59
- preferred_username
59
+ given_name,
60
+ family_name
60
61
  } = values;
61
62
  const learnLang = values["custom:learnLang"] || "";
62
63
  const forLang = values["custom:forLang"] || "";
@@ -85,7 +86,7 @@ function GeneralForm({
85
86
  };
86
87
  getLearnLangLabel();
87
88
  }, [role, t]);
88
- const isPreferredUsernameDisabled = username && (username.toLowerCase().includes("wayside") || username.toLowerCase().includes("google"));
89
+ const isNameDisabled = username && (username.toLowerCase().includes("wayside") || username.toLowerCase().includes("google"));
89
90
  return /*#__PURE__*/_jsxs(Grid, {
90
91
  container: true,
91
92
  sx: {
@@ -102,46 +103,54 @@ function GeneralForm({
102
103
  },
103
104
  children: t("required_field")
104
105
  })
105
- }), !isGeneratedEmail && /*#__PURE__*/_jsx(Grid, {
106
+ }), /*#__PURE__*/_jsx(Grid, {
106
107
  size: {
107
108
  xs: 12,
108
109
  md: 6
109
110
  },
110
111
  children: /*#__PURE__*/_jsx(TextField, {
111
- id: "email",
112
- name: "email",
113
- label: t("email"),
114
- value: email,
112
+ id: "given_name",
113
+ name: "given_name",
114
+ label: t("first_name"),
115
+ title: t("first_name"),
116
+ value: given_name,
115
117
  onChange: handleChange,
116
118
  onBlur: handleBlur,
117
- helperText: touched.email ? t(errors.email) : "",
118
- error: touched.email && Boolean(errors.email),
119
- type: "email",
119
+ helperText: touched.given_name ? t(errors.given_name) : "",
120
+ error: touched.given_name && Boolean(errors.given_name),
121
+ type: "text",
120
122
  variant: "outlined",
121
- disabled: true,
122
123
  fullWidth: true,
123
- required: true
124
+ required: true,
125
+ disabled: isNameDisabled,
126
+ slotProps: {
127
+ htmlInput: {
128
+ "data-lpignore": "true",
129
+ autoComplete: "off",
130
+ maxLength: 32
131
+ }
132
+ }
124
133
  })
125
134
  }), /*#__PURE__*/_jsx(Grid, {
126
135
  size: {
127
136
  xs: 12,
128
- md: isGeneratedEmail ? 12 : 6
137
+ md: 6
129
138
  },
130
139
  children: /*#__PURE__*/_jsx(TextField, {
131
- id: "preferred_username",
132
- name: "preferred_username",
133
- label: t("name"),
134
- title: t("name"),
135
- value: preferred_username,
140
+ id: "family_name",
141
+ name: "family_name",
142
+ label: t("last_name"),
143
+ title: t("last_name"),
144
+ value: family_name,
136
145
  onChange: handleChange,
137
146
  onBlur: handleBlur,
138
- helperText: touched.preferred_username ? t(errors.preferred_username) : "",
139
- error: touched.preferred_username && Boolean(errors.preferred_username),
147
+ helperText: touched.family_name ? t(errors.family_name) : "",
148
+ error: touched.family_name && Boolean(errors.family_name),
140
149
  type: "text",
141
150
  variant: "outlined",
142
151
  fullWidth: true,
143
152
  required: true,
144
- disabled: isPreferredUsernameDisabled,
153
+ disabled: isNameDisabled,
145
154
  slotProps: {
146
155
  htmlInput: {
147
156
  "data-lpignore": "true",
@@ -150,6 +159,26 @@ function GeneralForm({
150
159
  }
151
160
  }
152
161
  })
162
+ }), !isGeneratedEmail && /*#__PURE__*/_jsx(Grid, {
163
+ size: {
164
+ xs: 12,
165
+ md: 6
166
+ },
167
+ children: /*#__PURE__*/_jsx(TextField, {
168
+ id: "email",
169
+ name: "email",
170
+ label: t("email"),
171
+ value: email,
172
+ onChange: handleChange,
173
+ onBlur: handleBlur,
174
+ helperText: touched.email ? t(errors.email) : "",
175
+ error: touched.email && Boolean(errors.email),
176
+ type: "email",
177
+ variant: "outlined",
178
+ disabled: true,
179
+ fullWidth: true,
180
+ required: true
181
+ })
153
182
  }), /*#__PURE__*/_jsx(Grid, {
154
183
  size: {
155
184
  xs: 12,
@@ -241,7 +270,8 @@ export default function General({
241
270
  t = text => text,
242
271
  initialValues = {
243
272
  picture: null,
244
- name: "Unknown",
273
+ given_name: "",
274
+ family_name: "",
245
275
  email: "Unknown",
246
276
  city: "",
247
277
  country: "",
@@ -261,8 +291,11 @@ export default function General({
261
291
  }) {
262
292
  const {
263
293
  email,
294
+ given_name,
295
+ family_name,
264
296
  preferred_username
265
297
  } = initialValues;
298
+ const displayName = [given_name, family_name].filter(Boolean).join(" ");
266
299
  const {
267
300
  classes
268
301
  } = useStyles();
@@ -271,7 +304,8 @@ export default function General({
271
304
  const [avatarOptionClicked, setAvatarOptionClicked] = useState(false);
272
305
  const [confirm] = useConfirm(t);
273
306
  const generalFormInitialValues = {
274
- preferred_username,
307
+ given_name,
308
+ family_name,
275
309
  email,
276
310
  "custom:learnLang": initialValues["custom:learnLang"],
277
311
  "custom:forLang": initialValues["custom:forLang"],
@@ -316,12 +350,12 @@ export default function General({
316
350
  display: "inline-block"
317
351
  },
318
352
  children: [values.picture ? /*#__PURE__*/_jsx(Avatar, {
319
- "aria-label": preferred_username,
353
+ "aria-label": displayName,
320
354
  src: values.picture,
321
355
  className: classes.avatar,
322
356
  alt: ""
323
357
  }) : /*#__PURE__*/_jsx(Avatar, {
324
- "aria-label": preferred_username,
358
+ "aria-label": displayName,
325
359
  sx: theme => ({
326
360
  backgroundColor: theme.palette.mode === "dark" ? "white" : "",
327
361
  color: theme.palette.mode === "dark" ? theme.palette.background.default : ""
@@ -378,7 +412,12 @@ export default function General({
378
412
  variant: "h5",
379
413
  component: "h5",
380
414
  gutterBottom: true,
381
- children: preferred_username
415
+ title: displayName || preferred_username,
416
+ noWrap: true,
417
+ sx: {
418
+ maxWidth: "100%"
419
+ },
420
+ children: displayName || preferred_username
382
421
  }), !isGeneratedEmail && /*#__PURE__*/_jsx(Typography, {
383
422
  color: "textSecondary",
384
423
  variant: "body1",
@@ -527,7 +566,8 @@ const isEmail = value => {
527
566
  return !emailRegex.test(value);
528
567
  };
529
568
  General.validationSchema = Yup.object().shape({
530
- preferred_username: Yup.string().required("required").test("noSpecialChars", "no_special_characters", value => !containsInvalidSymbols(value)).test("is-not-email", "Email addresses are not allowed", value => isEmail(value)),
569
+ given_name: Yup.string().required("required").test("noSpecialChars", "no_special_characters", value => !containsInvalidSymbols(value)).test("is-not-email", "Email addresses are not allowed", value => isEmail(value)),
570
+ family_name: Yup.string().required("required").test("noSpecialChars", "no_special_characters", value => !containsInvalidSymbols(value)).test("is-not-email", "Email addresses are not allowed", value => isEmail(value)),
531
571
  name: Yup.string().test("noSpecialChars", "no_special_characters", value => !containsInvalidSymbols(value)).nullable(),
532
572
  "custom:forLang": Yup.string().nullable(),
533
573
  "custom:learnLang": Yup.string().nullable()
@@ -27,7 +27,8 @@ function Exercise({
27
27
  sectionId,
28
28
  topicId,
29
29
  setIsExerciseSelected,
30
- phrases
30
+ phrases,
31
+ topicName
31
32
  }) {
32
33
  const noRoleplays = name.toLowerCase() === "roleplays" && roleplays.length === 0;
33
34
  const noPhrases = name.toLowerCase() === "phrases" && !phrases?.length && !getPhraseLists;
@@ -96,7 +97,8 @@ function Exercise({
96
97
  isExerciseSelected: isExerciseSelected,
97
98
  setIsExerciseSelected: setIsExerciseSelected,
98
99
  isExpanded: phraseListsOpen,
99
- onPhraseListsLoaded: handlePhraseListsLoaded
100
+ onPhraseListsLoaded: handlePhraseListsLoaded,
101
+ topicName: topicName
100
102
  })
101
103
  })
102
104
  }), name.toLowerCase() === "roleplays" && /*#__PURE__*/_jsx(Collapse, {
@@ -114,7 +116,8 @@ function Exercise({
114
116
  t: t,
115
117
  isExerciseSelected: isExerciseSelected,
116
118
  handleSelectExercise: handleSelectExercise,
117
- useCase: useCase
119
+ useCase: useCase,
120
+ topicName: topicName
118
121
  })
119
122
  })
120
123
  })]
@@ -340,6 +343,7 @@ function Topic({
340
343
  handleStartExercise: handleStartExercise,
341
344
  isCreator: isCreator,
342
345
  getPhraseLists: getPhraseLists,
346
+ topicName: topicName,
343
347
  ...otherProps
344
348
  })
345
349
  })]
@@ -15,7 +15,8 @@ import { searchStringToObj, objToSearchString } from "../../utils";
15
15
  import FlashOnIcon from "@mui/icons-material/FlashOn";
16
16
  import FlashOffIcon from "@mui/icons-material/FlashOff";
17
17
  import MenuBookIcon from "@mui/icons-material/MenuBook";
18
- import { Collapse, ListItemText, ListItemIcon, List, ListItemAvatar, IconButton, Badge, Tooltip, Box, Chip, LinearProgress } from "@mui/material";
18
+ import { Collapse, ListItemText, ListItemIcon, List, ListItemAvatar, IconButton, Badge, Tooltip, Box, Chip } from "@mui/material";
19
+ import ColorLinearProgress from "../../Misc/ColorLinearProgress/ColorLinearProgress";
19
20
  import Add from "@mui/icons-material/Add";
20
21
  import Face from "@mui/icons-material/Face";
21
22
  import ForumIcon from "@mui/icons-material/Forum";
@@ -181,13 +182,9 @@ function ItemProgressBar({
181
182
  sx: {
182
183
  width: 160
183
184
  },
184
- children: /*#__PURE__*/_jsx(LinearProgress, {
185
+ children: /*#__PURE__*/_jsx(ColorLinearProgress, {
185
186
  variant: "determinate",
186
- value: pct,
187
- sx: {
188
- height: 6,
189
- borderRadius: 3
190
- }
187
+ value: pct
191
188
  })
192
189
  }), /*#__PURE__*/_jsx(Typography, {
193
190
  variant: "caption",
@@ -1,15 +1,17 @@
1
- import { useEffect, useState, Fragment } from "react";
1
+ import { useEffect, useMemo, useState } from "react";
2
2
  import PropTypes from "prop-types";
3
- import { Avatar, Box, Button, IconButton, List, ListItem, ListItemAvatar, ListItemText, ListSubheader, Typography, Tooltip, Menu, MenuItem, Chip } from "@mui/material";
3
+ import { Avatar, Box, Button, IconButton, List, ListItem, ListItemAvatar, ListItemText, ListSubheader, TextField, Typography, Tooltip, Menu, MenuItem, Chip } from "@mui/material";
4
4
  import { makeStyles } from "tss-react/mui";
5
5
  import Person from "@mui/icons-material/Person";
6
6
  import CommentIcon from "@mui/icons-material/Comment";
7
7
  import MoreVertIcon from "@mui/icons-material/MoreVert";
8
+ import SortIcon from "@mui/icons-material/Sort";
8
9
  import Skeleton from "@mui/material/Skeleton";
9
10
  import MembersNotFound from "../../Misc/NotFound/Members";
10
11
  import { DateTime } from "luxon";
11
12
  import useConfirm from "../../hooks/useConfirm";
12
13
  import HtmlTooltipComponent from "../../Misc/HtmlTooltipComponent/HtmlTooltipComponent";
14
+ import { getStudentSortOrder, setStudentSortOrder, compareMembersBySortOrder, MEMBERS_SORT_ORDER_KEY } from "../../utils/studentSort";
13
15
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
14
16
  const useStyles = makeStyles()(theme => ({
15
17
  root: {
@@ -116,7 +118,7 @@ function Member(props) {
116
118
  })
117
119
  })
118
120
  }), /*#__PURE__*/_jsx(ListItemText, {
119
- primary: featureFlags.classroomModes.readWrite && featureFlags.classroomModes.challenge && member.assignedLabels?.isReadWriteModeStudent && member.assignedLabels?.isChallengeModeStudent ? /*#__PURE__*/_jsxs(Box, {
121
+ primary: featureFlags.classroomModes?.readWrite && featureFlags.classroomModes?.challenge && member.assignedLabels?.isReadWriteModeStudent && member.assignedLabels?.isChallengeModeStudent ? /*#__PURE__*/_jsxs(Box, {
120
122
  sx: {
121
123
  display: "flex",
122
124
  alignItems: "center",
@@ -143,7 +145,7 @@ function Member(props) {
143
145
  color: "primary"
144
146
  })
145
147
  })]
146
- }) : featureFlags.classroomModes.readWrite && member.assignedLabels?.isReadWriteModeStudent ? /*#__PURE__*/_jsxs(Box, {
148
+ }) : featureFlags.classroomModes?.readWrite && member.assignedLabels?.isReadWriteModeStudent ? /*#__PURE__*/_jsxs(Box, {
147
149
  sx: {
148
150
  display: "flex",
149
151
  alignItems: "center",
@@ -160,7 +162,7 @@ function Member(props) {
160
162
  color: "primary"
161
163
  })
162
164
  })]
163
- }) : featureFlags.classroomModes.challenge && member.assignedLabels?.isChallengeModeStudent ? /*#__PURE__*/_jsxs(Box, {
165
+ }) : featureFlags.classroomModes?.challenge && member.assignedLabels?.isChallengeModeStudent ? /*#__PURE__*/_jsxs(Box, {
164
166
  sx: {
165
167
  display: "flex",
166
168
  alignItems: "center",
@@ -193,7 +195,7 @@ function Member(props) {
193
195
  }), /*#__PURE__*/_jsxs(OverflowMenu, {
194
196
  anchorEl: anchorEl,
195
197
  handleClose: handleCloseMenu,
196
- children: [featureFlags.classroomModes.readWrite && (member.assignedLabels?.isReadWriteModeStudent ? /*#__PURE__*/_jsx(HtmlTooltipComponent, {
198
+ children: [featureFlags.classroomModes?.readWrite && (member.assignedLabels?.isReadWriteModeStudent ? /*#__PURE__*/_jsx(HtmlTooltipComponent, {
197
199
  titleBold: t("heritage_speaker"),
198
200
  titleDesc: t("heritage_speaker_desc"),
199
201
  placement: "right",
@@ -229,7 +231,7 @@ function Member(props) {
229
231
  primary: t("mark_as_heritage_speaker")
230
232
  })
231
233
  })
232
- })), featureFlags.classroomModes.challenge && (member.assignedLabels?.isChallengeModeStudent ? /*#__PURE__*/_jsx(HtmlTooltipComponent, {
234
+ })), featureFlags.classroomModes?.challenge && (member.assignedLabels?.isChallengeModeStudent ? /*#__PURE__*/_jsx(HtmlTooltipComponent, {
233
235
  titleBold: t("challenge_student"),
234
236
  titleDesc: t("challenge_student_desc"),
235
237
  placement: "right",
@@ -405,13 +407,56 @@ function Members({
405
407
  handleDeleteClassroomMember,
406
408
  handleUpdateClassroomMember,
407
409
  isClassroomArchived,
408
- featureFlags = {}
410
+ featureFlags = {},
411
+ showSortControl = false
409
412
  }) {
410
- return /*#__PURE__*/_jsx(_Fragment, {
411
- children: /*#__PURE__*/_jsx(MemberList, {
413
+ const [sortOrder, setSortOrder] = useState(() => getStudentSortOrder(MEMBERS_SORT_ORDER_KEY, "joinDate"));
414
+ const handleSortOrderChange = value => {
415
+ setSortOrder(value);
416
+ setStudentSortOrder(MEMBERS_SORT_ORDER_KEY, value);
417
+ };
418
+ const sortedMembers = useMemo(() => [...(members || [])].sort((a, b) => compareMembersBySortOrder(a, b, sortOrder)), [members, sortOrder]);
419
+ return /*#__PURE__*/_jsxs(_Fragment, {
420
+ children: [showSortControl && t && /*#__PURE__*/_jsxs(Box, {
421
+ sx: theme => ({
422
+ display: "flex",
423
+ alignItems: "center",
424
+ justifyContent: "flex-end",
425
+ gap: 1,
426
+ px: 2,
427
+ py: 1,
428
+ borderBottom: `1px solid ${theme.palette.divider}`,
429
+ backgroundColor: theme.palette.mode === "dark" ? theme.palette.background.default : theme.palette.grey[50]
430
+ }),
431
+ children: [/*#__PURE__*/_jsx(SortIcon, {
432
+ fontSize: "small",
433
+ color: "action"
434
+ }), /*#__PURE__*/_jsxs(TextField, {
435
+ select: true,
436
+ size: "small",
437
+ id: "members_sort_by",
438
+ "data-cy": "members_sort_by",
439
+ label: t("sort_by"),
440
+ value: sortOrder,
441
+ onChange: e => handleSortOrderChange(e.target.value),
442
+ sx: {
443
+ minWidth: 170
444
+ },
445
+ children: [/*#__PURE__*/_jsx(MenuItem, {
446
+ value: "joinDate",
447
+ children: t("join_date")
448
+ }), /*#__PURE__*/_jsx(MenuItem, {
449
+ value: "firstName",
450
+ children: t("first_name")
451
+ }), /*#__PURE__*/_jsx(MenuItem, {
452
+ value: "lastName",
453
+ children: t("last_name")
454
+ })]
455
+ })]
456
+ }), /*#__PURE__*/_jsx(MemberList, {
412
457
  t: t,
413
458
  placeholderImageUrl: placeholderImageUrl,
414
- members: members ? members.sort((a, b) => a.createdAt < b.createdAt ? 1 : -1) : [],
459
+ members: sortedMembers,
415
460
  handleMessage: handleMessage,
416
461
  disableSubheader: disableSubheader,
417
462
  getMemberDetails: getMemberDetails,
@@ -420,7 +465,7 @@ function Members({
420
465
  handleUpdateClassroomMember: handleUpdateClassroomMember,
421
466
  isClassroomArchived: isClassroomArchived,
422
467
  featureFlags: featureFlags
423
- })
468
+ })]
424
469
  });
425
470
  }
426
471
  Members.propTypes = {
@@ -1,20 +1,81 @@
1
- import { render } from "@testing-library/react";
2
- import { describe, it } from "vitest";
1
+ import { render, screen, cleanup } from "@testing-library/react";
2
+ import { describe, it, expect, afterEach } from "vitest";
3
3
  import { ThemeProvider, createTheme } from "@mui/material/styles";
4
4
  import Members from "./Members";
5
5
  import { props } from "./Members.stories";
6
+ import { MEMBERS_SORT_ORDER_KEY } from "../../utils/studentSort";
6
7
  import { jsx as _jsx } from "react/jsx-runtime";
8
+ const renderMembers = (extraProps = {}) => {
9
+ const theme = createTheme();
10
+ return render(/*#__PURE__*/_jsx(ThemeProvider, {
11
+ theme: theme,
12
+ children: /*#__PURE__*/_jsx(Members, {
13
+ ...props,
14
+ featureFlags: {
15
+ classroomModes: {}
16
+ },
17
+ ...extraProps
18
+ })
19
+ }));
20
+ };
21
+ const renderedUsernames = async () => {
22
+ await screen.findByText("Peter Parker");
23
+ const knownUsernames = new Set(props.members.map(m => m.username));
24
+ return Array.from(document.querySelectorAll(".MuiListItemText-primary")).map(el => el.textContent).filter(text => knownUsernames.has(text));
25
+ };
26
+ afterEach(() => {
27
+ cleanup();
28
+ localStorage.removeItem(MEMBERS_SORT_ORDER_KEY);
29
+ });
7
30
  describe("Members", () => {
8
31
  it("renders without crashing", () => {
9
- const theme = createTheme();
10
32
  const {
11
33
  unmount
12
- } = render(/*#__PURE__*/_jsx(ThemeProvider, {
13
- theme: theme,
14
- children: /*#__PURE__*/_jsx(Members, {
15
- ...props
16
- })
17
- }));
34
+ } = renderMembers();
35
+ unmount();
36
+ });
37
+ it("defaults to join date order (newest first)", async () => {
38
+ const {
39
+ unmount
40
+ } = renderMembers({
41
+ showSortControl: true
42
+ });
43
+ const usernames = await renderedUsernames();
44
+ expect(usernames).toEqual(["Indigo Young", "Indigo Garland", "Cellophane", "Spider", "Evelyn Tool", "Oran Tool", "Peter Parker", "Mary Jane", "Giles Posture", "Indigo Violet", "Norman Gordon", "Penny Tool"]);
45
+ unmount();
46
+ });
47
+ it("sorts by last name when the stored preference is lastName", async () => {
48
+ localStorage.setItem(MEMBERS_SORT_ORDER_KEY, "lastName");
49
+ const {
50
+ unmount
51
+ } = renderMembers({
52
+ showSortControl: true
53
+ });
54
+ const usernames = await renderedUsernames();
55
+ expect(usernames).toEqual(["Cellophane", "Indigo Garland", "Norman Gordon", "Mary Jane", "Peter Parker", "Giles Posture", "Spider", "Evelyn Tool", "Oran Tool", "Penny Tool", "Indigo Violet", "Indigo Young"]);
56
+ unmount();
57
+ });
58
+ it("sorts by first name when the stored preference is firstName", async () => {
59
+ localStorage.setItem(MEMBERS_SORT_ORDER_KEY, "firstName");
60
+ const {
61
+ unmount
62
+ } = renderMembers({
63
+ showSortControl: true
64
+ });
65
+ const usernames = await renderedUsernames();
66
+ expect(usernames).toEqual(["Cellophane", "Evelyn Tool", "Giles Posture", "Indigo Garland", "Indigo Violet", "Indigo Young", "Mary Jane", "Norman Gordon", "Oran Tool", "Penny Tool", "Peter Parker", "Spider"]);
67
+ unmount();
68
+ });
69
+ it("does not mutate the members prop", async () => {
70
+ localStorage.setItem(MEMBERS_SORT_ORDER_KEY, "lastName");
71
+ const originalOrder = props.members.map(m => m.memberId);
72
+ const {
73
+ unmount
74
+ } = renderMembers({
75
+ showSortControl: true
76
+ });
77
+ await screen.findByText("Peter Parker");
78
+ expect(props.members.map(m => m.memberId)).toEqual(originalOrder);
18
79
  unmount();
19
80
  });
20
81
  });
@@ -18,7 +18,8 @@ function PhraseList({
18
18
  setIsPhraseListSelected,
19
19
  t,
20
20
  isExerciseSelected,
21
- setIsExerciseSelected
21
+ setIsExerciseSelected,
22
+ topicName
22
23
  }) {
23
24
  const [open, setOpen] = useState(false);
24
25
  const isTranslationHidden = phraseList?.hiddenPhraseGroupGames?.translation;
@@ -37,7 +38,8 @@ function PhraseList({
37
38
  timer: 30,
38
39
  answerOption: 0,
39
40
  phraseListId: phraseList.phraseListId,
40
- phrases: phraseList.phrases || []
41
+ phrases: phraseList.phrases || [],
42
+ topicName: topicName ?? ""
41
43
  });
42
44
  };
43
45
  return /*#__PURE__*/_jsxs("div", {
@@ -162,7 +164,8 @@ function PhraseListSelection({
162
164
  isExerciseSelected,
163
165
  isExpanded,
164
166
  setIsExerciseSelected,
165
- onPhraseListsLoaded
167
+ onPhraseListsLoaded,
168
+ topicName
166
169
  }) {
167
170
  const [isPhraseListSelected, setIsPhraseListSelected] = useState(false);
168
171
  const phraseListsQuery = courseQuerys.useCourseSectionTopicPhraseLists(async (cId, sId, tId, filters) => getPhraseLists(cId, sId, tId, filters), {
@@ -232,7 +235,8 @@ function PhraseListSelection({
232
235
  setIsPhraseListSelected: setIsPhraseListSelected,
233
236
  t: t,
234
237
  isExerciseSelected: isExerciseSelected,
235
- setIsExerciseSelected: setIsExerciseSelected
238
+ setIsExerciseSelected: setIsExerciseSelected,
239
+ topicName: topicName
236
240
  }, phraseList.phraseListId || index));
237
241
  }
238
242
  export default PhraseListSelection;
@@ -15,7 +15,8 @@ function Roleplay({
15
15
  setIsRoleplaySelected,
16
16
  t,
17
17
  isExerciseSelected,
18
- handleSelectExercise = null
18
+ handleSelectExercise = null,
19
+ topicName
19
20
  }) {
20
21
  const [open, setOpen] = useState(false);
21
22
  const games = ["roleplay-story", "roleplay-fill-in-the-blanks", "roleplay-act-it-out", "roleplay-act-it-out-listening", "roleplay-act-it-out-speaking", "roleplay-act-it-out-listening-speaking"];
@@ -26,7 +27,8 @@ function Roleplay({
26
27
  exercise: exerciseName,
27
28
  timer: 30,
28
29
  answerOption: 0,
29
- roleplayId: roleplay.roleplayId
30
+ roleplayId: roleplay.roleplayId,
31
+ topicName: topicName ?? ""
30
32
  });
31
33
  };
32
34
  const isRoleplayEmpty = roleplay?.script?.length === 0 || !roleplay?.script?.some(line => line.text !== undefined);
@@ -174,7 +176,8 @@ function RoleplaySelection({
174
176
  t,
175
177
  isExerciseSelected,
176
178
  handleSelectExercise,
177
- useCase
179
+ useCase,
180
+ topicName
178
181
  }) {
179
182
  const [isRoleplaySelected, setIsRoleplaySelected] = useState(false);
180
183
  return roleplays.map((roleplay, index) => /*#__PURE__*/_jsx(Roleplay, {
@@ -186,7 +189,8 @@ function RoleplaySelection({
186
189
  t: t,
187
190
  isExerciseSelected: isExerciseSelected,
188
191
  handleSelectExercise: handleSelectExercise,
189
- useCase: useCase
192
+ useCase: useCase,
193
+ topicName: topicName
190
194
  }, index));
191
195
  }
192
196
  export default RoleplaySelection;