Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
387 changes: 375 additions & 12 deletions classrooms.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/components/ClassroomExtraMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { connect } from 'react-redux';
import { State as ReduxState } from '../state';
import LocalizedString from '../util/LocalizedString';
import { AsyncClassroom } from '../state/State/Classroom';
import { classroomNameAsString } from '../util/classroomDisplayName';
import Async from 'state/State/Async';
import TourTarget from './Tours/TourTarget';
import { TourRegistry } from '../tours/TourRegistry';
Expand Down Expand Up @@ -121,7 +122,7 @@ class ClassroomExtraMenu extends React.PureComponent<Props, State> {
return (
<TourTarget registry={this.props.tourRegistry} targetKey='classroom-extra-options-dropdown'>
<Container theme={theme} style={style} className={className}>
<ItemLabel theme={theme}><ItemIcon icon={faUsersRectangle} /> {LocalizedString.lookup(tr('Current Classroom: '), locale)} {latestClassroom?.classroomId || LocalizedString.lookup(tr('None'), locale)}</ItemLabel>
<ItemLabel theme={theme}><ItemIcon icon={faUsersRectangle} /> {LocalizedString.lookup(tr('Current Classroom: '), locale)} {latestClassroom ? classroomNameAsString(latestClassroom.classroomId, locale) : LocalizedString.lookup(tr('None'), locale)}</ItemLabel>
<ItemLabel theme={theme}><ItemIcon icon={faPersonChalkboard} /> {LocalizedString.lookup(tr('Classroom Teacher: '), locale)} {latestClassroom?.teacherDisplayName || LocalizedString.lookup(tr('None'), locale)}</ItemLabel>
{latestClassroom
? (<TourTarget registry={this.props.tourRegistry} targetKey='leave-classroom'>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Classrooms/ChallengeTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ class ChallengeTabView extends React.Component<Props, State> {
theme={theme}
view={view}
currentStudentDisplayName={currentStudentDisplayName}
currentClassroom={currentStudentClassroom}
currentClassroom={view === 'teacherView' ? currentSelectedClassroom : currentStudentClassroom}
tourRegistry={this.props.tourRegistry} />
</TourTarget>
</SectionsColumn>)
Expand Down
25 changes: 16 additions & 9 deletions src/components/Classrooms/CreateAssignmentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,17 @@ const CreateAssignmentView = ({
const assignButtonTourActive =
!originalAssignment && activeTourStepId === 'teacher-create-assignment-assign';

function selectedStudentsLabel() {
const selectedCount = Object.keys(selectedStudents).length;
const rosterCount = Object.keys(loadedClassroom?.studentIds || {}).length;
if (rosterCount === 0) {
return `0 ${LocalizedString.lookup(tr('Students'), locale)}`;
}
return selectedCount === rosterCount
? LocalizedString.lookup(tr('All Students'), locale)
: `${selectedCount} ${LocalizedString.lookup(tr('Students'), locale)}`;
}

function resolveStudentsForAssign(): Dict<{ id: string, displayName: string, assignments?: Dict<ClassroomAssignment> }> {
if (Object.keys(selectedStudents).length > 0) {
return selectedStudents;
Expand Down Expand Up @@ -418,7 +429,7 @@ const CreateAssignmentView = ({
? !(Object.keys(selectedStudents).length > 0)
: !(
assignButtonTourActive ||
(enableAssign && Object.keys(selectedStudents).length > 0)
enableAssign
);

return (
Expand All @@ -445,7 +456,7 @@ const CreateAssignmentView = ({
onEditComplete?.(selectedStudents, editedAssignment);
handleEdit(editedAssignment);
})()
: (enableAssign && Object.keys(selectedStudents).length > 0) || assignButtonTourActive
: enableAssign || assignButtonTourActive
? runCreateAssign()
: null;
}}>
Expand All @@ -464,7 +475,7 @@ const CreateAssignmentView = ({
onEditComplete?.(selectedStudents, editedAssignment);
handleEdit(editedAssignment);
})()
: (enableAssign && Object.keys(selectedStudents).length > 0) || assignButtonTourActive
: enableAssign || assignButtonTourActive
? runCreateAssign()
: null;
}}>
Expand Down Expand Up @@ -536,17 +547,13 @@ const CreateAssignmentView = ({
<TourTarget registry={tourRegistry} targetKey="teacher-create-assignment-assign-to-button" style={{ display: 'contents' }}>
<Button
style={{ marginLeft: '1.4em' }} theme={theme} onClick={() => setAssignToMenuVisible(true)}>
{Object.keys(selectedStudents).length === Object.keys(loadedClassroom?.studentIds || {}).length
? LocalizedString.lookup(tr('All Students'), locale)
: `${Object.keys(selectedStudents).length} ${LocalizedString.lookup(tr('Students'), locale)}`}
{selectedStudentsLabel()}
</Button>
</TourTarget>
) : (
<Button
style={{ marginLeft: '1.4em' }} theme={theme} onClick={() => setAssignToMenuVisible(true)}>
{Object.keys(selectedStudents).length === Object.keys(loadedClassroom?.studentIds || {}).length
? LocalizedString.lookup(tr('All Students'), locale)
: `${Object.keys(selectedStudents).length} ${LocalizedString.lookup(tr('Students'), locale)}`}
{selectedStudentsLabel()}
</Button>
)}
</AssignmentInfoRow>
Expand Down
7 changes: 5 additions & 2 deletions src/components/Classrooms/PeopleView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,11 @@ const PeopleView = ({
function getStudents(currentSelectedClassroom: AsyncClassroom | null) {

const loadedClassroom = Async.latestValue(currentSelectedClassroom);
const stateClassroom = classroomList[loadedClassroom?.docId || ''];
const students = Async.latestValue(stateClassroom)?.studentIds;
const docId = loadedClassroom?.docId || '';
const stateClassroom = docId ? classroomList[docId] : undefined;
const students =
Async.latestValue(stateClassroom)?.studentIds ??
loadedClassroom?.studentIds;

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5em', fontSize: '1.5em', alignItems: 'flex-start', width: '80%' }}>
Expand Down
69 changes: 55 additions & 14 deletions src/components/Dialog/LeaveClassDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ import { I18nAction } from '../../state/reducer';
import { connect } from 'react-redux';
import Form from '../interface/Form';
import { Classroom } from 'state/State/Classroom';
import {
classroomNameAsString,
classroomNamesMatch,
} from '../../util/classroomDisplayName';

export interface LeaveClassDialogPublicProps extends ThemeProps, StyleProps {

locale: LocalizedString.Language;
onClose: () => void;
currentClassroom: Classroom;
onLeaveClassDialogClose: () => void;
onLeaveClassDialogClose: () => Promise<void>;
}

interface LeaveClassDialogPrivateProps {
Expand All @@ -25,6 +29,7 @@ interface LeaveClassDialogPrivateProps {
}

interface LeaveClassDialogState {
errorMessage: string;
}

type Props = LeaveClassDialogPublicProps & LeaveClassDialogPrivateProps;
Expand All @@ -51,33 +56,65 @@ const StyledForm = styled(Form, (props: ThemeProps) => ({
paddingRight: `${props.theme.itemPadding * 2}px`,
}));

const ErrorMessage = styled('div', (props: ThemeProps) => ({
color: '#ff6b6b',
fontSize: '0.9em',
textAlign: 'center',
}));

export class LeaveClassDialog extends React.PureComponent<Props, State> {
/** Exact name shown in the dialog — user must match this string. */
private readonly confirmClassroomName_: string;

constructor(props: Props) {
super(props);
this.confirmClassroomName_ = classroomNameAsString(
props.currentClassroom.classroomId,
props.locale
);
this.state = { errorMessage: '' };
}

onFinalize_ = (values: { [id: string]: string }) => {
onFinalize_ = async (values: { [id: string]: string }) => {
const { leaveClassName } = values;
const { currentClassroom } = this.props;
const entered = typeof leaveClassName === 'string' ? leaveClassName : '';
if (!classroomNamesMatch(entered, this.confirmClassroomName_)) {
this.setState({
errorMessage: LocalizedString.lookup(
tr('Classroom name does not match. Type the classroom name exactly as shown above.'),
this.props.locale
),
});
return;
}
try {
if (leaveClassName === currentClassroom.classroomId) {
this.props.onLeaveClassDialogClose();
} else {
return;
}
this.setState({ errorMessage: '' });
await this.props.onLeaveClassDialogClose();
} catch (error) {
console.error('Error leaving classroom:', error);
this.setState({
errorMessage: LocalizedString.lookup(
tr('Could not leave the classroom. Please try again.'),
this.props.locale
),
});
}


};

render() {
const { props } = this;
const { style, className, theme, onClose, locale, currentClassroom } = props;
const { props, state } = this;
const { style, className, theme, onClose, locale } = props;
const { errorMessage: leaveError } = state;
const displayName = this.confirmClassroomName_;
const LEAVECLASSROOM_FORM_ITEMS: Form.Item[] = [
Form.leaveClass('leaveClassName', LocalizedString.lookup(tr('Leave Classroom'), locale), LocalizedString.lookup(tr('Reenter classroom name to confirm leaving classroom.'), locale)),
Form.leaveClass(
'leaveClassName',
LocalizedString.lookup(tr('Leave Classroom'), locale),
LocalizedString.lookup(
tr('Type the classroom name shown above to confirm.'),
locale
)
),
];

return (
Expand All @@ -89,8 +126,12 @@ export class LeaveClassDialog extends React.PureComponent<Props, State> {
<Container theme={theme} style={style} className={className}>

<div style={{ display: 'flex', flexDirection: 'row', gap: '0.25em', alignItems: 'center' }}>
{LocalizedString.lookup(tr('Are you sure you want to leave: '), locale)}<ClassroomName theme={theme}>{currentClassroom.classroomId}</ClassroomName>?
{LocalizedString.lookup(tr('Are you sure you want to leave: '), locale)}
<ClassroomName theme={theme}>{displayName}</ClassroomName>?
</div>
{leaveError ? (
<ErrorMessage theme={theme}>{leaveError}</ErrorMessage>
) : null}
<StyledForm
theme={theme}
onFinalize={this.onFinalize_}
Expand Down
4 changes: 3 additions & 1 deletion src/components/Tours/GuidedTour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,13 @@ class GuidedTour extends React.PureComponent<Props, State> {
return;
}

// if steps changed, clamp index
// if steps changed, clamp index and remeasure (e.g. placement updated for same step id)
if (prevProps.steps !== this.props.steps) {
const max = Math.max(0, this.props.steps.length - 1);
if (this.state.stepIndex > max) {
this.setState({ stepIndex: max }, () => this.measure(true));
} else if (this.props.isOpen) {
this.measure(false);
}
}

Expand Down
10 changes: 7 additions & 3 deletions src/components/interface/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,16 @@ class Form extends React.PureComponent<Form.Props, Form.State> {
const { items } = props;
const { values } = state;

const ret = {};
const ret: { [id: string]: unknown } = {};
for (const item of items) {
ret[item.id] = item.finalizer(values[item.id].text);
if (!(item.id in values)) continue;
const finalizer = item.finalizer ?? Form.IDENTITY_FINALIZER;
ret[item.id] = finalizer(values[item.id].text);
}

this.props.onFinalize(ret);
void Promise.resolve(this.props.onFinalize(ret)).catch((error: unknown) => {
console.error('Form onFinalize error:', error);
});
};

private isFinalizeAllowed_ = () => {
Expand Down
Loading
Loading