UNPKG

@craftercms/studio-ui

Version:

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

274 lines (272 loc) 10.7 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 GlobalAppToolbar from '../GlobalAppToolbar'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import React, { useRef, useState } from 'react'; import { Box } from '@mui/material'; import { fetchConfigurationXML, writeConfiguration } from '../../services/configuration'; import AceEditor from '../AceEditor/AceEditor'; import useStyles from './styles'; import SecondaryButton from '../SecondaryButton'; import PrimaryButton from '../PrimaryButton'; import { forkJoin } from 'rxjs'; import { ConditionalLoadingState } from '../LoadingState/LoadingState'; import ConfigurationSamplePreviewDialog from '../ConfigurationSamplePreviewDialog'; import ConfirmDropdown from '../ConfirmDropdown'; import { useDispatch } from 'react-redux'; import { showSystemNotification } from '../../state/actions/system'; import { useMount } from '../../hooks/useMount'; import Paper from '@mui/material/Paper'; import { MAX_CONFIG_SIZE } from '../../utils/constants'; import { MaxLengthCircularProgress } from '../MaxLengthCircularProgress'; import useUnmount from '../../hooks/useUnmount'; import useActiveUser from '../../hooks/useActiveUser'; import { batchActions, dispatchDOMEvent } from '../../state/actions/misc'; import { closeConfirmDialog, showConfirmDialog } from '../../state/actions/dialogs'; import { createCustomDocumentEventListener } from '../../utils/dom'; import { useBeforeUnload, useNavigate } from 'react-router-dom'; import { GlobalRoutes } from '../../env/routes'; const translations = defineMessages({ configSaved: { id: 'globalConfig.configSaved', defaultMessage: 'Configuration saved successfully.' }, documentError: { id: 'globalConfig.documentError', defaultMessage: 'The document contains errors. Check for error markers on side of the editor.' } }); export function GlobalConfigManagement() { const [content, setContent] = useState(''); const [sample, setSample] = useState(''); const [lastSavedContent, setLastSavedContent] = useState(''); const [enable, setEnable] = useState(true); const [viewSample, setViewSample] = useState(false); const [hasChanges, setHasChanges] = useState(false); const hasChangesRef = useRef(hasChanges); hasChangesRef.current = hasChanges; const { classes } = useStyles(); const [contentSize, setContentSize] = useState(0); const aceEditorRef = useRef(); const dispatch = useDispatch(); const { formatMessage } = useIntl(); const { username } = useActiveUser(); const navigate = useNavigate(); const sessionStorageKey = `craftercms.${username}.globalConfigContent`; useMount(() => { const requests = [ fetchConfigurationXML('studio_root', '/configuration/samples/sample-studio-config-override.yaml', 'studio'), fetchConfigurationXML('studio_root', '/configuration/studio-config-override.yaml', 'studio') ]; forkJoin(requests).subscribe(([sample, content]) => { const sessionContent = sessionStorage.getItem(sessionStorageKey); setLastSavedContent(content); if (sessionContent) { setContent(sessionContent); setContentSize(sessionContent.length); setHasChanges(true); sessionStorage.removeItem(sessionStorageKey); } else { setContent(content); setContentSize(content.length); } setSample(sample); setEnable(false); }); }); useUnmount(() => { if (hasChangesRef.current) { sessionStorage.setItem(sessionStorageKey, aceEditorRef.current.getValue()); const eventId = 'unsavedConfigManagementChangesConfirmation'; dispatch( showConfirmDialog({ body: React.createElement(FormattedMessage, { defaultMessage: 'You left unsaved changes. Go back and continue editing?' }), onCancel: batchActions([closeConfirmDialog(), dispatchDOMEvent({ id: eventId, button: 'cancel' })]), onOk: batchActions([closeConfirmDialog(), dispatchDOMEvent({ id: eventId, button: 'ok' })]), okButtonText: React.createElement(FormattedMessage, { defaultMessage: 'Continue editing' }), cancelButtonText: React.createElement(FormattedMessage, { defaultMessage: 'Discard changes' }) }) ); createCustomDocumentEventListener(eventId, ({ button }) => { if (button === 'ok') { navigate(GlobalRoutes.GlobalConfig); } else { sessionStorage.removeItem(sessionStorageKey); } }); } }); useBeforeUnload((event) => { if (hasChangesRef.current) { event.preventDefault(); } }); const onUseSampleClick = (type) => { if (type === 'replace') { setContent(sample); } else { const currentContent = aceEditorRef.current.getValue(); setContent(currentContent + sample); } setViewSample(false); }; const onResetClick = () => { aceEditorRef.current.setValue(lastSavedContent, -1); // sets cursor in position 0, avoiding all editor content selection aceEditorRef.current.focus(); }; const onSaveClick = () => { const value = aceEditorRef.current.getValue(); const errors = aceEditorRef.current .getSession() .getAnnotations() .filter((annotation) => { return annotation.type === 'error'; }); if (errors.length) { dispatch( showSystemNotification({ message: formatMessage(translations.documentError), options: { variant: 'error' } }) ); } else { writeConfiguration('studio_root', '/configuration/studio-config-override.yaml', 'studio', value).subscribe( () => { setLastSavedContent(value); setHasChanges(false); dispatch( showSystemNotification({ message: formatMessage(translations.configSaved) }) ); }, ({ response: { response } }) => { dispatch( showSystemNotification({ message: response.message, options: { variant: 'error' } }) ); } ); setHasChanges(false); } }; const onChange = () => { const currentEditorValue = aceEditorRef.current.getValue(); const hasChanges = lastSavedContent !== currentEditorValue; setContentSize(currentEditorValue.length); setHasChanges(hasChanges); }; const onAceInit = (editor) => { editor.commands.addCommand({ name: 'saveToCrafter', bindKey: { win: 'Ctrl-S', mac: 'Command-S' }, exec: () => onSaveClick(), readOnly: false }); }; return React.createElement( Paper, { elevation: 0 }, React.createElement(GlobalAppToolbar, { title: React.createElement(FormattedMessage, { id: 'globalMenu.globalConfigEntryLabel', defaultMessage: 'Global Config' }) }), React.createElement( ConditionalLoadingState, { isLoading: enable }, React.createElement( 'section', { className: classes.paper }, React.createElement(AceEditor, { ref: aceEditorRef, onChange: onChange, classes: { editorRoot: classes.root }, value: content, mode: 'ace/mode/yaml', theme: 'ace/theme/textmate', autoFocus: true, readOnly: enable, onInit: onAceInit }), React.createElement( Box, { p: '10px', display: 'flex', justifyContent: 'space-between' }, React.createElement( SecondaryButton, { onClick: () => setViewSample(true) }, React.createElement(FormattedMessage, { id: 'globalConfig.viewSample', defaultMessage: 'View Sample' }) ), React.createElement(ConfirmDropdown, { disabled: !hasChanges, classes: { button: classes.marginLeftAuto }, text: React.createElement(FormattedMessage, { id: 'words.reset', defaultMessage: 'Reset' }), cancelText: React.createElement(FormattedMessage, { id: 'words.cancel', defaultMessage: 'Cancel' }), confirmText: React.createElement(FormattedMessage, { id: 'words.ok', defaultMessage: 'Ok' }), confirmHelperText: React.createElement(FormattedMessage, { id: 'globalConfig.confirmHelper', defaultMessage: 'Discard unsaved changes?' }), onConfirm: onResetClick }), React.createElement( PrimaryButton, { disabled: !hasChanges || contentSize > MAX_CONFIG_SIZE, onClick: onSaveClick }, React.createElement(FormattedMessage, { id: 'words.save', defaultMessage: 'Save' }) ), React.createElement(MaxLengthCircularProgress, { sxs: { root: { ml: 1 }, circularProgress: { width: '35px !important', height: '35px !important' } }, max: MAX_CONFIG_SIZE, current: contentSize, renderThresholdPercentage: 90 }) ) ) ), React.createElement(ConfigurationSamplePreviewDialog, { onUseSampleClick: onUseSampleClick, open: viewSample, onClose: () => setViewSample(false), onClosed: () => aceEditorRef.current.focus(), content: sample }) ); } export default GlobalConfigManagement;