UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

310 lines (308 loc) 11.8 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { isBlank } from '../../utils/string'; import { update } from '../../services/sites'; import { fetchSites } from '../../state/actions/sites'; import { closeSingleFileUploadDialog, showSingleFileUploadDialog, updateEditSiteDialog } from '../../state/actions/dialogs'; import { batchActions, dispatchDOMEvent } from '../../state/actions/misc'; import { showErrorDialog } from '../../state/reducers/dialogs/error'; import { ConditionalLoadingState } from '../LoadingState/LoadingState'; import useProjectPreviewImage from '../../hooks/useProjectPreviewImage'; import useUpdateRefs from '../../hooks/useUpdateRefs'; import { FormattedMessage, useIntl } from 'react-intl'; import DialogBody from '../DialogBody'; import Grid from '@mui/material/Grid'; import TextField from '@mui/material/TextField'; import Card from '@mui/material/Card'; import CardMedia from '@mui/material/CardMedia'; import CardActions from '@mui/material/CardActions'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; import EditRoundedIcon from '@mui/icons-material/EditRounded'; import DialogFooter from '../DialogFooter'; import SecondaryButton from '../SecondaryButton'; import PrimaryButton from '../PrimaryButton'; import { PROJECT_PREVIEW_IMAGE_UPDATED } from '../../utils/constants'; import { showSystemNotification } from '../../state/actions/system'; export function EditSiteDialogContainer(props) { const { site, onClose, onSaveSuccess, onSiteImageChange, isSubmitting } = props; const [hasNameConflict, setHasNameConflict] = useState(false); const sites = useSelector((state) => state.sites.byId); const dispatch = useDispatch(); const originalName = site.name; const [name, setName] = useState(originalName); const originalDescription = site.description ?? ''; const [description, setDescription] = useState(originalDescription); const [imageUrl, fetch] = useProjectPreviewImage( site.id, '/studio/static-assets/themes/cstudioTheme/images/default-contentType.jpg' ); const disableSubmit = hasNameConflict || (originalName === name.trim() && originalDescription === description.trim()) || isBlank(name); const { formatMessage } = useIntl(); function checkSiteName(value) { if ( (originalName !== value && sites && Object.keys(sites).filter((key) => sites[key].name === value).length) || value.trim() === '' ) { setHasNameConflict(true); } else { setHasNameConflict(false); } } const handleSubmit = (id, name, description) => { if (!disableSubmit) { dispatch(updateEditSiteDialog({ isSubmitting: true })); update({ id, name: name.trim(), description: description.trim() }).subscribe({ next(response) { dispatch( batchActions([ updateEditSiteDialog({ hasPendingChanges: false, isSubmitting: false }), fetchSites() ]) ); onSaveSuccess?.(response); }, error({ response: { response } }) { dispatch(batchActions([updateEditSiteDialog({ isSubmitting: false }), showErrorDialog({ error: response })])); } }); } }; const onCloseButtonClick = (e) => onClose(e, null); const onSiteNameChange = (value) => { checkSiteName(value); setName(value); dispatch( updateEditSiteDialog({ hasPendingChanges: originalDescription !== description.trim() || originalName !== value.trim() }) ); }; const onKeyPress = (event) => { if (event.key === 'Enter') { handleSubmit(site.id, name, description); } }; const onSiteDescriptionChange = (value) => { setDescription(value); dispatch( updateEditSiteDialog({ hasPendingChanges: originalName !== name.trim() || originalDescription !== value.trim() }) ); }; const onEditSiteImage = () => { dispatch( showSingleFileUploadDialog({ path: '/.crafter/screenshots', site: site.id, customFileName: 'default.png', fileTypes: ['image/png'], onClose: closeSingleFileUploadDialog(), onUploadComplete: batchActions([ closeSingleFileUploadDialog(), dispatchDOMEvent({ id: PROJECT_PREVIEW_IMAGE_UPDATED }) ]) }) ); }; const handleEditSiteImageCompleteRef = useUpdateRefs(() => { dispatch( showSystemNotification({ message: formatMessage({ defaultMessage: 'Preview image updated successfully.' }) }) ); onSiteImageChange?.(); fetch(); }); const onSubmit = () => handleSubmit(site.id, name, description); useEffect(() => { const callback = () => { handleEditSiteImageCompleteRef.current(); }; document.addEventListener(PROJECT_PREVIEW_IMAGE_UPDATED, callback); return () => { document.removeEventListener(PROJECT_PREVIEW_IMAGE_UPDATED, callback); }; }, [handleEditSiteImageCompleteRef]); return React.createElement( ConditionalLoadingState, { isLoading: !site }, React.createElement( React.Fragment, null, React.createElement( DialogBody, null, React.createElement( Grid, { container: true, spacing: 2 }, React.createElement( Grid, { item: true, sm: 6 }, React.createElement( Card, { elevation: 2 }, React.createElement(CardMedia, { component: 'img', image: imageUrl, title: name, sx: { height: '234px' } }), React.createElement( CardActions, { sx: { placeContent: 'center' }, disableSpacing: true }, React.createElement( Tooltip, { title: React.createElement(FormattedMessage, { id: 'words.edit', defaultMessage: 'Edit' }) }, React.createElement( IconButton, { onClick: onEditSiteImage }, React.createElement(EditRoundedIcon, null) ) ) ) ) ), React.createElement( Grid, { item: true, sm: 6 }, React.createElement( Grid, { container: true, spacing: 1, component: 'form' }, React.createElement( Grid, { item: true, xs: 12 }, React.createElement(TextField, { autoFocus: true, fullWidth: true, id: 'name', name: 'name', label: React.createElement(FormattedMessage, { id: 'editSiteDialog.siteName', defaultMessage: 'Project Name' }), onChange: (event) => onSiteNameChange(event.target.value), onKeyDown: onKeyPress, value: name, inputProps: { maxLength: 255 }, error: hasNameConflict, helperText: !name.trim() ? formatMessage({ id: 'editSiteDialog.siteNameRequired', defaultMessage: 'Project Name is required.' }) : hasNameConflict ? formatMessage({ id: 'editSiteDialog.sitenameExists', defaultMessage: 'The name already exist.' }) : '' }) ), React.createElement( Grid, { item: true, xs: 12 }, React.createElement(TextField, { id: 'siteId', name: 'id', label: React.createElement(FormattedMessage, { id: 'editSiteDialog.siteId', defaultMessage: 'Project ID' }), fullWidth: true, value: site.id, disabled: true, helperText: React.createElement(FormattedMessage, { id: 'editSiteDialog.notEditable', defaultMessage: 'The project id is not editable' }) }) ), React.createElement( Grid, { item: true, xs: 12 }, React.createElement(TextField, { id: 'description', name: 'description', label: React.createElement(FormattedMessage, { id: 'editSiteDialog.siteDescription', defaultMessage: 'Project Description' }), fullWidth: true, multiline: true, onChange: (event) => onSiteDescriptionChange(event.target.value), onKeyPress: (e) => { // This behaviour is kind of backwards from how it's usually seen in text editors. // Perhaps we should flip it to shift/ctrl + enter creating new lines and only enter submitting? if (e.key !== 'Enter' || e.ctrlKey || e.shiftKey) { onKeyPress?.(e); } }, value: description ?? '', inputProps: { maxLength: 4000 } }) ) ) ) ) ), React.createElement( DialogFooter, null, onCloseButtonClick && React.createElement( SecondaryButton, { onClick: onCloseButtonClick, variant: 'contained', disabled: isSubmitting }, React.createElement(FormattedMessage, { id: 'editSiteDialog.cancel', defaultMessage: 'Cancel' }) ), onSubmit && React.createElement( PrimaryButton, { onClick: () => onSubmit(), variant: 'contained', color: 'primary', loading: isSubmitting, disabled: disableSubmit }, React.createElement(FormattedMessage, { id: 'words.submit', defaultMessage: 'Submit' }) ) ) ) ); }