@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
310 lines (308 loc) • 11.8 kB
JavaScript
/*
* 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' })
)
)
)
);
}