@selfcommunity/react-ui
Version:
React UI Components to integrate a Community created with SelfCommunity Platform.
301 lines (296 loc) • 19.7 kB
JavaScript
import { __rest } from "tslib";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { LoadingButton } from '@mui/lab';
import { Box, CardActionArea, Card, CardContent, FormGroup, Paper, TextField, Typography, Chip, styled } from '@mui/material';
import { useThemeProps } from '@mui/system';
import { CourseService, formatHttpErrorCode } from '@selfcommunity/api-services';
import { SCPreferences, UserUtils, useSCPaymentsEnabled, useSCPreferences, useSCUser } from '@selfcommunity/react-core';
import { SCContentType, SCCoursePrivacyType, SCCourseTypologyType } from '@selfcommunity/types';
import { Logger } from '@selfcommunity/utils';
import classNames from 'classnames';
import PubSub from 'pubsub-js';
import { Fragment, useCallback, useMemo, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { SCOPE_SC_UI } from '../../constants/Errors';
import { SCCourseEventType, SCTopicType } from '../../constants/PubSub';
import { PREFIX } from './constants';
import UploadCourseCover from './UploadCourseCover';
import { COURSE_DESCRIPTION_MAX_LENGTH, COURSE_TITLE_MAX_LENGTH, SCCourseFormStepType } from '../../constants/Course';
import CategoryAutocomplete from '../CategoryAutocomplete';
import CourseEdit from './Edit';
import CoursePublicationDialog from './Dialog';
import PaywallsConfigurator from '../PaywallsConfigurator';
import { ContentAccessType } from '../PaywallsConfigurator/constants';
const messages = defineMessages({
name: {
id: 'ui.courseForm.name.placeholder',
defaultMessage: 'ui.courseForm.name.placeholder'
},
description: {
id: 'ui.courseForm.description.placeholder',
defaultMessage: 'ui.courseForm.description.placeholder'
},
categoryEmpty: {
id: 'ui.courseForm.category.placeholder.empty',
defaultMessage: 'ui.courseForm.category.placeholder.empty'
},
category: {
id: 'ui.courseForm.category.placeholder',
defaultMessage: 'ui.courseForm.category.placeholder'
}
});
const classes = {
root: `${PREFIX}-root`,
actions: `${PREFIX}-actions`,
card: `${PREFIX}-card`,
content: `${PREFIX}-content`,
cover: `${PREFIX}-cover`,
description: `${PREFIX}-description`,
error: `${PREFIX}-error`,
form: `${PREFIX}-form`,
frequency: `${PREFIX}-frequency`,
name: `${PREFIX}-name`,
privacySection: `${PREFIX}-privacy-section`,
privacySectionInfo: `${PREFIX}-privacy-section-info`,
selected: `${PREFIX}-selected`,
disabled: `${PREFIX}-disabled`,
stepOne: `${PREFIX}-step-one`,
stepTwo: `${PREFIX}-step-two`,
stepCustomization: `${PREFIX}-step-customization`,
cardTitle: `${PREFIX}-card-title`,
title: `${PREFIX}-title`,
contrastColor: `${PREFIX}-contrast-color`,
paywallsConfiguratorWrap: `${PREFIX}-paywalls-configurator-wrap`
};
const Root = styled(Box, {
name: PREFIX,
slot: 'Root'
})(() => ({}));
/**
*> API documentation for the Community-JS Course Form component. Learn about the available props and the CSS API.
*
#### Import
```jsx
import {CourseForm} from '@selfcommunity/react-ui';
```
#### Component Name
The name `SCCourseForm` can be used when providing style overrides in the theme.
#### CSS
|Rule Name|Global class|Description|
|---|---|---|
| root | .SCCourseForm-root | Styles applied to the root element. |
| actions | .SCCourseForm-actions | Styles applied to the actions element. |
| card | .SCCourseForm-card | Styles applied to the card element. |
| content | .SCCourseForm-content | Styles applied to the content element. |
| cover | .SCCourseForm-cover | Styles applied to the cover element. |
| description | .SCCourseForm-description | Styles applied to the description element. |
| error | .SCCourseForm-error | Styles applied to the error element. |
| form | .SCCourseForm-form | Styles applied to the form element. |
| frequency | .SCCourseForm-frequency | Styles applied to the frequency element. |
| name | .SCCourseForm-name | Styles applied to the name element. |
| privacySection | .SCCourseForm-privacy-section | Styles applied to the privacy section. |
| privacySectionInfo | .SCCourseForm-privacy-section-info | Styles applied to the privacy section info. |
| selected | .SCCourseForm-selected | Styles applied to the selected element. |
| stepOne | .SCCourseForm-step-one | Styles applied to the step-one element. |
| stepTwo | .SCCourseForm-step-two | Styles applied to the step-two element. |
| title | .SCCourseForm-title | Styles applied to the title element. |
* @param inProps
*/
export default function CourseForm(inProps) {
var _a, _b, _c, _d, _e;
//PROPS
const props = useThemeProps({
props: inProps,
name: PREFIX
});
const { className, onSuccess, onError, course = null, step = SCCourseFormStepType.GENERAL, hidePaywalls = false, onStepChange } = props, rest = __rest(props, ["className", "onSuccess", "onError", "course", "step", "hidePaywalls", "onStepChange"]);
// INTL
const intl = useIntl();
const initialFieldState = {
imageOriginal: (course === null || course === void 0 ? void 0 : course.image_bigger) || '',
imageOriginalFile: '',
name: (course === null || course === void 0 ? void 0 : course.name) || '',
type: (course === null || course === void 0 ? void 0 : course.type) || '',
description: course ? course.description : '',
categories: course ? course.categories : [],
privacy: course ? course.privacy : '',
products: ((_a = course === null || course === void 0 ? void 0 : course.paywalls) === null || _a === void 0 ? void 0 : _a.map((p) => p.id)) || [],
contentAccessType: ((_b = course === null || course === void 0 ? void 0 : course.paywalls) === null || _b === void 0 ? void 0 : _b.length) ? ContentAccessType.PAID : ContentAccessType.FREE,
isSubmitting: false
};
// STATE
const [field, setField] = useState(initialFieldState);
const [_step, setStep] = useState(step);
const [error, setError] = useState({});
const [openDialog, setOpenDialog] = useState(false);
// CONTEXT
const scUserContext = useSCUser();
// PREFERENCES
const { preferences } = useSCPreferences();
// PAYMENTS
const { isPaymentsEnabled } = useSCPaymentsEnabled();
const isStaff = useMemo(() => scUserContext.user && UserUtils.isStaff(scUserContext.user), [scUserContext.user]);
const courseAdvancedEnabled = useMemo(() => preferences[SCPreferences.CONFIGURATIONS_COURSES_ADVANCED_ENABLED].value, [preferences]);
const _backgroundCover = Object.assign({}, (field.imageOriginal
? { background: `url('${field.imageOriginal}') center / cover` }
: { background: `url('${preferences[SCPreferences.IMAGES_USER_DEFAULT_COVER].value}') center / cover` }));
const handleChangeCover = useCallback((cover) => {
setField((prev) => (Object.assign(Object.assign({}, prev), { ['imageOriginalFile']: cover })));
const reader = new FileReader();
reader.onloadend = () => {
setField((prev) => (Object.assign(Object.assign({}, prev), { ['imageOriginal']: reader.result })));
};
reader.readAsDataURL(cover);
if (error.imageOriginalError) {
delete error.imageOriginalError;
setError(error);
}
}, [error]);
/**
* Handles step change
* @param newStep
*/
const handleChangeStep = (newStep) => {
setStep(newStep);
onStepChange(newStep, field.type);
};
/**
* Formats categories object to a specific format needed in the form body
* @param data
*/
function convertToCategoriesObject(data) {
const categories = {};
data.forEach((category, index) => {
categories[`categories[${index}]`] = category.id;
});
return categories;
}
/**
* Handle change category
* @param categories
*/
const handleOnChangeCategory = (categories) => {
setField((prev) => (Object.assign(Object.assign({}, prev), { ['categories']: convertToCategoriesObject(categories) })));
};
/**
* Notify when a group info changed
* @param data
*/
const notifyChanges = useCallback((data) => {
if (course) {
// Edit group
PubSub.publish(`${SCTopicType.COURSE}.${SCCourseEventType.EDIT}`, data);
}
else {
// Create group
PubSub.publish(`${SCTopicType.COURSE}.${SCCourseEventType.CREATE}`, data);
}
}, [course]);
/**
* Handles the form submission for create/update action
*/
const handleSubmit = useCallback(() => {
var _a;
setField((prev) => (Object.assign(Object.assign({}, prev), { isSubmitting: true })));
const formData = new FormData();
if (field.imageOriginalFile) {
formData.append('image_original', field.imageOriginalFile);
}
formData.append('name', field.name);
formData.append('description', field.description);
formData.append('type', field.type);
if (field.privacy) {
formData.append('privacy', field.privacy);
}
if (field.categories) {
for (const key in field.categories) {
formData.append(key, field.categories[key]);
}
}
if (field.products.length && field.contentAccessType === ContentAccessType.PAID && (isStaff || (course && ((_a = course.paywalls) === null || _a === void 0 ? void 0 : _a.length)))) {
formData.append(`products`, JSON.stringify(field.products));
}
else {
formData.append(`products`, '[]');
}
let courseService;
if (course) {
courseService = CourseService.patchCourse(course.id, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
else {
courseService = CourseService.createCourse(formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
courseService
.then((data) => {
notifyChanges(data);
setField((prev) => (Object.assign(Object.assign({}, prev), { isSubmitting: false })));
onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(data);
})
.catch((e) => {
const _error = formatHttpErrorCode(e);
if (Object.values(_error)[0]['error'] === 'unique') {
setError(Object.assign(Object.assign({}, error), { nameError: _jsx(FormattedMessage, { id: "ui.courseForm.name.error.unique", defaultMessage: "ui.courseForm.name.error.unique" }) }));
}
else {
setError(Object.assign(Object.assign({}, error), _error));
}
setField((prev) => (Object.assign(Object.assign({}, prev), { isSubmitting: false })));
Logger.error(SCOPE_SC_UI, e);
onError === null || onError === void 0 ? void 0 : onError(e);
});
}, [course, field, onSuccess, onError, notifyChanges]);
/**
* Handles course fields change
*/
const handleChange = useCallback((course) => {
const { name, value } = course.target;
setField((prev) => (Object.assign(Object.assign({}, prev), { [name]: value })));
if (error[`${name}Error`]) {
delete error[`${name}Error`];
setError(error);
}
}, [setField, error]);
/**
* Handle change content access tyoe
* @param type
*/
const handleChangeContentAccessType = (type) => {
setField((prev) => (Object.assign(Object.assign({}, prev), { contentAccessType: type })));
};
/**
* Handle change payment products
* @param products
*/
const handleChangePaymentsProducts = (products) => {
setField((prev) => (Object.assign(Object.assign({}, prev), { products: products.map((product) => product.id) })));
};
/**
* Handles for closing confirm dialog
*/
const handleClose = useCallback(() => {
setOpenDialog(false);
}, [setOpenDialog]);
/**
* Renders root object
*/
return (_jsxs(Fragment, { children: [_jsx(Root, Object.assign({ className: classNames(classes.root, className) }, rest, { children: _jsxs(Box, Object.assign({ className: _step === SCCourseFormStepType.GENERAL ? classes.stepOne : classes.stepTwo }, { children: [_step === SCCourseFormStepType.GENERAL && (_jsx(Fragment, { children: Object.values(SCCourseTypologyType).map((option, index) => (_jsx(Card, Object.assign({ className: classNames(classes.card, { [classes.selected]: option === field.type }, { [classes.disabled]: !courseAdvancedEnabled && option !== SCCourseTypologyType.SELF }) }, { children: _jsx(CardActionArea, Object.assign({ onClick: () => setField((prev) => (Object.assign(Object.assign({}, prev), { ['type']: option }))) }, { children: _jsxs(CardContent, { children: [_jsxs(Typography, Object.assign({ variant: "subtitle2", className: classes.cardTitle }, { children: [_jsx(FormattedMessage, { id: `ui.courseForm.${option}.title`, defaultMessage: `ui.courseForm.${option}.title` }), !courseAdvancedEnabled && option !== SCCourseTypologyType.SELF && (_jsx(Chip, { variant: "outlined", color: "warning", size: "small", label: _jsx(FormattedMessage, { id: "ui.courseForm.comingSoon.chip", defaultMessage: "ui.courseForm.comingSoon.chip" }) }))] })), _jsx(Typography, Object.assign({ variant: "body2" }, { children: _jsx(FormattedMessage, { id: `ui.courseForm.${option}.info`, defaultMessage: `ui.courseForm.${option}.info` }) }))] }) })) }), index))) })), _step === SCCourseFormStepType.CUSTOMIZATION && (_jsxs(Fragment, { children: [course && (_jsx(Typography, Object.assign({ variant: "h5", className: classes.contrastColor }, { children: _jsx(FormattedMessage, { id: "ui.courseForm.edit.title.general", defaultMessage: "ui.courseForm.edit.title.general" }) }))), _jsxs(FormGroup, Object.assign({ className: classNames(classes.form, _step === SCCourseFormStepType.CUSTOMIZATION && course ? classes.stepCustomization : undefined) }, { children: [_jsx(Paper, Object.assign({ style: _backgroundCover, classes: { root: classes.cover } }, { children: _jsx(UploadCourseCover, { isUploading: field.isSubmitting, onChange: handleChangeCover }) })), _jsx(TextField, { required: true, className: classes.name, placeholder: `${intl.formatMessage(messages.name)}`, margin: "normal", value: field.name, name: "name", onChange: handleChange, InputProps: {
endAdornment: _jsx(Typography, Object.assign({ variant: "body2" }, { children: COURSE_TITLE_MAX_LENGTH - field.name.length }))
}, error: Boolean((!!course && !field.name) || field.name.length > COURSE_TITLE_MAX_LENGTH) || Boolean(error['nameError']), helperText: !!course && !field.name ? (_jsx(FormattedMessage, { id: "ui.courseForm.required", defaultMessage: "ui.courseForm.required" })) : field.name.length > COURSE_TITLE_MAX_LENGTH ? (_jsx(FormattedMessage, { id: "ui.courseForm.name.error.maxLength", defaultMessage: "ui.courseForm.name.error.maxLength" })) : error['nameError'] ? (error['nameError']) : null }), _jsx(TextField, { multiline: true, className: classes.description, placeholder: `${intl.formatMessage(messages.description)}`, margin: "normal", value: field.description, name: "description", onChange: handleChange, InputProps: {
endAdornment: (_jsx(Typography, Object.assign({ variant: "body2" }, { children: ((_c = field.description) === null || _c === void 0 ? void 0 : _c.length) ? COURSE_DESCRIPTION_MAX_LENGTH - field.description.length : COURSE_DESCRIPTION_MAX_LENGTH })))
}, error: Boolean((!!field.privacy && !field.description) || ((_d = field.description) === null || _d === void 0 ? void 0 : _d.length) > COURSE_DESCRIPTION_MAX_LENGTH), helperText: !!field.privacy && !field.description ? (_jsx(FormattedMessage, { id: "ui.courseForm.required", defaultMessage: "ui.courseForm.required" })) : (((_e = field.description) === null || _e === void 0 ? void 0 : _e.length) > COURSE_DESCRIPTION_MAX_LENGTH && (_jsx(FormattedMessage, { id: "ui.courseForm.description.error.maxLength", defaultMessage: "ui.courseForm.description.error.maxLength" }))) }), _jsx(CategoryAutocomplete, { defaultValue: field.categories, TextFieldProps: { label: intl.formatMessage(Object.keys(field.categories).length ? messages.category : messages.categoryEmpty) }, multiple: true, onChange: handleOnChangeCategory }), course && _jsx(CourseEdit, { course: course, onPrivacyChange: (privacy) => setField((prev) => (Object.assign(Object.assign({}, prev), { ['privacy']: privacy }))) }), isPaymentsEnabled && isStaff && !hidePaywalls && (_jsx(Box, Object.assign({ className: classes.paywallsConfiguratorWrap }, { children: _jsx(PaywallsConfigurator, Object.assign({}, (course && { contentId: course.id }), { contentType: SCContentType.COURSE, onChangeContentAccessType: handleChangeContentAccessType, onChangePaymentProducts: handleChangePaymentsProducts })) })))] }))] })), _jsx(Box, Object.assign({ className: classNames(classes.actions, _step === SCCourseFormStepType.CUSTOMIZATION && course ? classes.stepCustomization : undefined) }, { children: _jsx(LoadingButton, Object.assign({ size: "small", loading: field.isSubmitting, disabled: _step === SCCourseFormStepType.GENERAL
? !field.type || Object.keys(error).length !== 0
: _step === SCCourseFormStepType.CUSTOMIZATION &&
(!field.name ||
Object.keys(error).length !== 0 ||
field.name.length > COURSE_TITLE_MAX_LENGTH ||
field.description.length > COURSE_DESCRIPTION_MAX_LENGTH ||
(!!field.privacy && (!field.description || course.num_sections === 0 || course.num_lessons === 0))), variant: "contained", onClick: _step === SCCourseFormStepType.GENERAL
? () => handleChangeStep(SCCourseFormStepType.CUSTOMIZATION)
: field.privacy !== SCCoursePrivacyType.DRAFT && course.privacy === SCCoursePrivacyType.DRAFT
? () => setOpenDialog(true)
: handleSubmit, color: "primary" }, { children: course ? (_jsx(FormattedMessage, { id: "ui.courseForm.edit.action.save", defaultMessage: "ui.courseForm.edit.action.save" })) : _step === SCCourseFormStepType.GENERAL ? (_jsx(FormattedMessage, { id: "ui.courseForm.button.next", defaultMessage: "ui.courseForm.button.next" })) : (_jsx(FormattedMessage, { id: "ui.courseForm.button.create", defaultMessage: "ui.courseForm.button.create" })) })) }))] })) })), openDialog && _jsx(CoursePublicationDialog, { onSubmit: handleSubmit, onClose: handleClose })] }));
}