feat: add teams setting editor component
This commit adds a setting editor for teams that allows adding/removing and editing teams.
This commit is contained in:
committed by
Awais Jibran
parent
a047236f59
commit
7682a17758
@@ -23,25 +23,25 @@ import AppConfigFormDivider from '../discussions/app-config-form/apps/shared/App
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
import messages from './messages';
|
||||
|
||||
function AppSettingsForm({ formikProps, children }) {
|
||||
function AppSettingsForm({ formikProps, children, showForm }) {
|
||||
return children && (
|
||||
<TransitionReplace>
|
||||
{formikProps.values.enabled
|
||||
? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="app-disabled" />
|
||||
)}
|
||||
{showForm ? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="app-disabled" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
|
||||
AppSettingsForm.propTypes = {
|
||||
formikProps: PropTypes.shape({
|
||||
values: PropTypes.shape({ enabled: PropTypes.bool.isRequired }),
|
||||
}).isRequired,
|
||||
// Ignore the warning here since we're just passing along the props as-is and the child component should validate
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
formikProps: PropTypes.object.isRequired,
|
||||
showForm: PropTypes.bool.isRequired,
|
||||
children: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -101,6 +101,7 @@ function AppSettingsModal({
|
||||
appId,
|
||||
title,
|
||||
children,
|
||||
configureBeforeEnable,
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onClose,
|
||||
@@ -210,9 +211,9 @@ function AppSettingsModal({
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{formikProps.values.enabled && children
|
||||
{(formikProps.values.enabled || configureBeforeEnable) && children
|
||||
&& <AppConfigFormDivider marginAdj={{ default: 0, sm: 0 }} />}
|
||||
<AppSettingsForm formikProps={formikProps}>
|
||||
<AppSettingsForm formikProps={formikProps} showForm={formikProps.values.enabled || configureBeforeEnable}>
|
||||
{children}
|
||||
</AppSettingsForm>
|
||||
</AppSettingsModalBase>
|
||||
@@ -251,6 +252,7 @@ AppSettingsModal.propTypes = {
|
||||
enableAppLabel: PropTypes.string.isRequired,
|
||||
enableAppHelp: PropTypes.string.isRequired,
|
||||
learnMoreText: PropTypes.string.isRequired,
|
||||
configureBeforeEnable: PropTypes.bool,
|
||||
};
|
||||
|
||||
AppSettingsModal.defaultProps = {
|
||||
@@ -258,6 +260,7 @@ AppSettingsModal.defaultProps = {
|
||||
onSettingsSave: null,
|
||||
initialValues: {},
|
||||
validationSchema: {},
|
||||
configureBeforeEnable: false,
|
||||
};
|
||||
|
||||
export default injectIntl(AppSettingsModal);
|
||||
|
||||
105
src/pages-and-resources/teams/Settings.jsx
Normal file
105
src/pages-and-resources/teams/Settings.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import { FieldArray } from 'formik';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import { useAppSetting } from '../../utils';
|
||||
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
import TeamSetEditor, { TeamSetTypes, TeamSizes } from './TeamSetEditor';
|
||||
|
||||
function TeamSettings({
|
||||
intl,
|
||||
onClose,
|
||||
}) {
|
||||
const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration');
|
||||
const blankNewTopic = {
|
||||
name: '',
|
||||
description: '',
|
||||
type: 'open',
|
||||
maxTeamSize: TeamSizes.DEFAULT,
|
||||
id: null,
|
||||
};
|
||||
|
||||
const handleSettingsSave = (values) => {
|
||||
// For newly-added teams, fill in an id.
|
||||
const topics = values.topics.map(topic => ({
|
||||
id: topic.id || uuid(),
|
||||
name: topic.name,
|
||||
type: topic.type,
|
||||
description: topic.description,
|
||||
max_team_size: topic.maxTeamSize,
|
||||
}));
|
||||
saveSettings({ topics });
|
||||
};
|
||||
|
||||
return (
|
||||
<AppSettingsModal
|
||||
appId="teams"
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
enableAppHelp={intl.formatMessage(messages.enableTeamsHelp)}
|
||||
enableAppLabel={intl.formatMessage(messages.enableTeamsLabel)}
|
||||
learnMoreText={intl.formatMessage(messages.enableTeamsLink)}
|
||||
onClose={onClose}
|
||||
initialValues={{ ...teamsConfiguration }}
|
||||
validationSchema={{
|
||||
topics: Yup.array().of(
|
||||
Yup.object({
|
||||
id: Yup.string(),
|
||||
name: Yup.string().required().trim(),
|
||||
type: Yup.mixed().oneOf(Object.values(TeamSetTypes)),
|
||||
description: Yup.string().required().trim(),
|
||||
maxTeamSize: Yup.number().required().min(TeamSizes.MIN).max(TeamSizes.MAX),
|
||||
}),
|
||||
).default([]),
|
||||
}}
|
||||
onSettingsSave={handleSettingsSave}
|
||||
configureBeforeEnable
|
||||
>
|
||||
{
|
||||
({
|
||||
handleChange, handleBlur, values, errors, touched,
|
||||
}) => (
|
||||
<>
|
||||
<h5>{intl.formatMessage(messages.teamSets)}</h5>
|
||||
<FieldArray name="topics">
|
||||
{({ push, remove }) => (
|
||||
<div>
|
||||
{values.topics?.map((topic, index) => (
|
||||
<TeamSetEditor
|
||||
key={index}
|
||||
teamSet={topic}
|
||||
errors={errors.topics?.[index]}
|
||||
touched={touched.topics?.[index]}
|
||||
fieldNameCommonBase={`topics.${index}`}
|
||||
onDelete={() => remove(index)}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={() => push(blankNewTopic)}
|
||||
>
|
||||
<Icon src={Add} /> {intl.formatMessage(messages.addTeamSet)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</FieldArray>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</AppSettingsModal>
|
||||
);
|
||||
}
|
||||
|
||||
TeamSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TeamSettings);
|
||||
193
src/pages-and-resources/teams/TeamSetEditor.jsx
Normal file
193
src/pages-and-resources/teams/TeamSetEditor.jsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
Button, Collapsible, Form, TransitionReplace,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState } from 'react';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* Team sizes enum
|
||||
* @enum
|
||||
* @type {{MIN: number, MAX: number, DEFAULT: number}}
|
||||
*/
|
||||
export const TeamSizes = {
|
||||
DEFAULT: 50,
|
||||
MIN: 1,
|
||||
MAX: 500,
|
||||
};
|
||||
|
||||
/**
|
||||
* Team types enum
|
||||
* @enum
|
||||
* @type {{PRIVATE_MANAGED: string, PUBLIC_MANAGED: string, OPEN: string}}
|
||||
*/
|
||||
export const TeamSetTypes = {
|
||||
OPEN: 'open',
|
||||
PUBLIC_MANAGED: 'public_managed',
|
||||
PRIVATE_MANAGED: 'private_managed',
|
||||
};
|
||||
|
||||
// Maps a team type to its corresponding intl message
|
||||
const TeamTypeNameMessage = {
|
||||
[TeamSetTypes.OPEN]: messages.teamSetTypeOpen,
|
||||
[TeamSetTypes.PUBLIC_MANAGED]: messages.teamSetTypePublicManaged,
|
||||
[TeamSetTypes.PRIVATE_MANAGED]: messages.teamSetTypePrivateManaged,
|
||||
};
|
||||
|
||||
function TeamSetEditor({
|
||||
intl, teamSet, onDelete, onChange, onBlur, fieldNameCommonBase, errors, touched,
|
||||
}) {
|
||||
const [isDeleting, setDeleting] = useState(false);
|
||||
const initiateDeletion = () => setDeleting(true);
|
||||
const cancelDeletion = () => setDeleting(false);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
title={(
|
||||
<div className="d-flex flex-column small flex-shrink-1">
|
||||
<span>{intl.formatMessage(TeamTypeNameMessage[teamSet.type])}</span>
|
||||
<span className="h4 text-truncate">{teamSet.name || '<new>'}</span>
|
||||
<span className="text-truncate">{teamSet.description || '<new>'}</span>
|
||||
</div>
|
||||
)}
|
||||
// If there is no id, this is a new team, so automatically open it
|
||||
defaultOpen={teamSet.id === null}
|
||||
>
|
||||
<TransitionReplace>
|
||||
{isDeleting
|
||||
? (
|
||||
<div className="d-flex flex-column m-4" key="isDeleting">
|
||||
<h3>{intl.formatMessage(messages.teamSetDeleteHeading)}</h3>
|
||||
<p>{intl.formatMessage(messages.teamSetDeleteBody)}</p>
|
||||
<div className="d-flex flex-row justify-content-end">
|
||||
<Button variant="muted" size="sm" onClick={() => onDelete(teamSet)}>
|
||||
{intl.formatMessage(messages.delete)}
|
||||
</Button>
|
||||
<Button variant="muted" size="sm" onClick={cancelDeletion}>
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<React.Fragment key="isConfiguring">
|
||||
<Form.Group key="isConfiguring">
|
||||
<Form.Control
|
||||
name={`${fieldNameCommonBase}.name`}
|
||||
floatingLabel={intl.formatMessage(messages.teamSetFormNameLabel)}
|
||||
defaultValue={teamSet.name}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<Form.Text>{intl.formatMessage(messages.teamSetFormNameHelp)}</Form.Text>
|
||||
{(touched?.name && errors?.name) && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
{intl.formatMessage(messages.teamSetFormNameError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
name={`${fieldNameCommonBase}.description`}
|
||||
floatingLabel={intl.formatMessage(messages.teamSetFormDescriptionLabel)}
|
||||
defaultValue={teamSet.description}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<Form.Text>{intl.formatMessage(messages.teamSetFormDescriptionHelp)}</Form.Text>
|
||||
{(touched?.description && errors?.description) && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
{intl.formatMessage(messages.teamSetFormDescriptionError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name={`${fieldNameCommonBase}.type`}
|
||||
floatingLabel={intl.formatMessage(messages.teamSetFormTypeLabel)}
|
||||
defaultValue={teamSet.type}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{Object.values(TeamSetTypes).map(teamSetType => (
|
||||
<option value={teamSetType} key={teamSetType}>
|
||||
{intl.formatMessage(TeamTypeNameMessage[teamSetType])}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text>{intl.formatMessage(messages.teamSetFormTypeHelp)}</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
type="number"
|
||||
name={`${fieldNameCommonBase}.maxTeamSize`}
|
||||
floatingLabel={intl.formatMessage(messages.teamSetFormMaxSizeLabel)}
|
||||
defaultValue={teamSet.maxTeamSize}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<Form.Text>{intl.formatMessage(messages.teamSetFormMaxSizeHelp)}</Form.Text>
|
||||
{(touched?.maxTeamSize && errors?.maxTeamSize) && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
{intl.formatMessage(messages.teamSetFormMaxSizeError, {
|
||||
min: TeamSizes.MIN,
|
||||
max: TeamSizes.MAX,
|
||||
})}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<hr style={{ marginLeft: '-0.75rem', marginRight: '-0.5rem' }} />
|
||||
<Button variant="muted" className="p-0" onClick={initiateDeletion}>
|
||||
{intl.formatMessage(messages.delete)}
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</TransitionReplace>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export const teamSetShape = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
maxTeamSize: PropTypes.number.isRequired,
|
||||
});
|
||||
|
||||
TeamSetEditor.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
fieldNameCommonBase: PropTypes.string.isRequired,
|
||||
errors: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
maxTeamSize: PropTypes.string,
|
||||
}),
|
||||
touched: PropTypes.shape({
|
||||
name: PropTypes.bool,
|
||||
description: PropTypes.bool,
|
||||
maxTeamSize: PropTypes.bool,
|
||||
}),
|
||||
teamSet: teamSetShape.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
TeamSetEditor.defaultProps = {
|
||||
errors: {
|
||||
name: null,
|
||||
description: null,
|
||||
maxTeamSize: null,
|
||||
},
|
||||
touched: {
|
||||
name: false,
|
||||
description: false,
|
||||
maxTeamSize: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default injectIntl(TeamSetEditor);
|
||||
110
src/pages-and-resources/teams/messages.js
Normal file
110
src/pages-and-resources/teams/messages.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: {
|
||||
id: 'authoring.pagesAndResources.teams.heading',
|
||||
defaultMessage: 'Configure teams',
|
||||
},
|
||||
enableTeamsLabel: {
|
||||
id: 'authoring.pagesAndResources.teams.enableTeams.label',
|
||||
defaultMessage: 'Teams',
|
||||
},
|
||||
enableTeamsHelp: {
|
||||
id: 'authoring.pagesAndResources.teams.enableTeams.help',
|
||||
defaultMessage: `Define the structure of teams in your course by adding
|
||||
team-sets; groups focussed around specific topics you create`,
|
||||
},
|
||||
enableTeamsLink: {
|
||||
id: 'authoring.pagesAndResources.teams.enableTeams.link',
|
||||
defaultMessage: 'Learn more about the teams',
|
||||
},
|
||||
teamSets: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSets.heading',
|
||||
defaultMessage: 'Team-sets',
|
||||
},
|
||||
configureTeamSet: {
|
||||
id: 'authoring.pagesAndResources.teams.configureTeamSet.heading',
|
||||
defaultMessage: 'Configure team-set',
|
||||
},
|
||||
teamSetFormNameLabel: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.name.label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
teamSetFormNameHelp: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.name.help',
|
||||
defaultMessage: 'The name learners will see when interacting with your team-set',
|
||||
},
|
||||
teamSetFormNameError: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.name.error',
|
||||
defaultMessage: 'Team set name can\'t be blank',
|
||||
},
|
||||
teamSetFormDescriptionLabel: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.description.label',
|
||||
defaultMessage: 'Description',
|
||||
},
|
||||
teamSetFormDescriptionHelp: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.description.help',
|
||||
defaultMessage: 'Details about your team-set, displayed below the team-set name.',
|
||||
},
|
||||
teamSetFormDescriptionError: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.description.error',
|
||||
defaultMessage: 'Team set description can\'t be blank.',
|
||||
},
|
||||
teamSetFormTypeLabel: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.type.label',
|
||||
defaultMessage: 'Type',
|
||||
},
|
||||
teamSetFormTypeHelp: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.type.help',
|
||||
defaultMessage: 'Control who can see, create and join teams',
|
||||
},
|
||||
teamSetTypeOpen: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.types.open',
|
||||
defaultMessage: 'Open',
|
||||
},
|
||||
teamSetTypePublicManaged: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.types.public_managed',
|
||||
defaultMessage: 'Public Managed',
|
||||
},
|
||||
teamSetTypePrivateManaged: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.types.private_managed',
|
||||
defaultMessage: 'Private Managed',
|
||||
},
|
||||
teamSetFormMaxSizeLabel: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.maxSize.label',
|
||||
defaultMessage: 'Max team size',
|
||||
},
|
||||
teamSetFormMaxSizeHelp: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.maxSize.help',
|
||||
defaultMessage: 'The maximum number of learners that can join a team',
|
||||
},
|
||||
teamSetFormMaxSizeError: {
|
||||
id: 'authoring.pagesAndResources.teams.teamSet.maxSize.error',
|
||||
defaultMessage: 'The maximum team size must be a number between {min} and {max}',
|
||||
},
|
||||
addTeamSet: {
|
||||
id: 'authoring.pagesAndResources.teams.addTeamSet.button',
|
||||
defaultMessage: 'Add team-set',
|
||||
},
|
||||
delete: {
|
||||
id: 'authoring.pagesAndResources.teams.deleteTeamSet.delete.button',
|
||||
defaultMessage: 'Delete',
|
||||
},
|
||||
cancel: {
|
||||
id: 'authoring.pagesAndResources.teams.deleteTeamSet.cancel-delete.button',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
teamSetDeleteHeading: {
|
||||
id: 'authoring.pagesAndResources.teams.deleteTeamSet.heading',
|
||||
defaultMessage: 'Delete team-set?',
|
||||
},
|
||||
teamSetDeleteBody: {
|
||||
id: 'authoring.pagesAndResources.teams.deleteTeamSet.body',
|
||||
defaultMessage: `EdX recommends that you do not delete team-sets once your
|
||||
course is running. Your team-set will no longer be visible in the LMS and
|
||||
learners will not be able to leave teams associated with it. Please delete
|
||||
learners from teams before deleting the associated teamSet.`,
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
Reference in New Issue
Block a user