UNPKG

@craftercms/studio-ui

Version:

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

293 lines (291 loc) 11.5 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 { makeStyles } from 'tss-react/mui'; import useActiveSiteId from '../../hooks/useActiveSiteId'; import useSpreadState from '../../hooks/useSpreadState'; import useSubject from '../../hooks/useSubject'; import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import { FormattedMessage, useIntl } from 'react-intl'; import { fetchMarketplacePlugins, installMarketplacePlugin } from '../../services/marketplace'; import { debounceTime } from 'rxjs/operators'; import { blockUI, unblockUI } from '../../state/actions/system'; import { translations } from './translations'; import { batchActions } from '../../state/actions/misc'; import { showErrorDialog } from '../../state/reducers/dialogs/error'; import DialogHeader from '../DialogHeader'; import DialogBody from '../DialogBody/DialogBody'; import PluginDetailsView from '../PluginDetailsView'; import SearchBar from '../SearchBar/SearchBar'; import DialogFooter from '../DialogFooter/DialogFooter'; import Pagination from '../Pagination'; import PluginList from './PluginList'; import PluginParametersForm from '../PluginParametersForm/PluginParametersForm'; import SecondaryButton from '../SecondaryButton'; import PrimaryButton from '../PrimaryButton'; import { UNDEFINED } from '../../utils/constants'; import LoadingState from '../LoadingState/LoadingState'; import ApiResponseErrorState from '../ApiResponseErrorState/ApiResponseErrorState'; const useStyles = makeStyles()(() => ({ searchWrapper: { marginBottom: '16px' } })); export function InstallPluginDialogContainer(props) { const siteId = useActiveSiteId(); const { installPermission = false, onInstall, installedPlugins = {} } = props; const [keyword, setKeyword] = useState(''); const [debouncedKeyword, setDebouncedKeyword] = useState(keyword); const [plugins, setPlugins] = useState(null); const [showSearchBar, setShowSearchBar] = useState(false); const [isFetching, setIsFetching] = useState(null); const [error, setError] = useState(null); const [offset, setOffset] = useState(0); const [limit, setLimit] = useState(9); const [selectedDetailsPlugin, setSelectedDetailsPlugin] = useState(null); const [formPluginState, setFormPluginState] = useSpreadState({ plugin: null, fields: {}, submitted: false, error: {} }); const { classes } = useStyles(); const onSearch$ = useSubject(); const dispatch = useDispatch(); const { formatMessage } = useIntl(); const [installingLookup, setInstallingLookup] = useSpreadState({}); const fetchPlugins = useCallback(() => { setError(null); setIsFetching(true); fetchMarketplacePlugins({ type: 'site', keywords: debouncedKeyword, limit, offset }).subscribe({ next(plugins) { setPlugins(plugins); setIsFetching(false); }, error(error) { setError(error.response.response); setIsFetching(false); } }); }, [debouncedKeyword, limit, offset]); useEffect(() => { fetchPlugins(); }, [fetchPlugins]); useEffect(() => { const subscription = onSearch$.pipe(debounceTime(400)).subscribe((keywords) => { setDebouncedKeyword(keywords); }); return () => { subscription.unsubscribe(); }; }, [onSearch$]); const onToggleSearchBar = () => { setShowSearchBar(!showSearchBar); }; const onSearch = (keyword, event) => { if (/^[\w\s-]*$/.test(event.target.value)) { onSearch$.next(keyword); setKeyword(keyword); } }; const onPluginDetails = (plugin) => { setSelectedDetailsPlugin(plugin); }; const onPluginDetailsClose = () => { setSelectedDetailsPlugin(null); }; const onInstallPlugin = (plugin, parameters) => { setInstallingLookup({ [plugin.id]: true }); dispatch(blockUI({ message: formatMessage(translations.installing, { name: plugin.name }) })); installMarketplacePlugin(siteId, plugin.id, plugin.version, parameters).subscribe({ next() { setInstallingLookup({ [plugin.id]: false }); onInstall(plugin); onPluginFormClose(); dispatch(unblockUI()); }, error({ response }) { setInstallingLookup({ [plugin.id]: false }); dispatch(batchActions([showErrorDialog({ error: response.response }), unblockUI()])); } }); }; const onPluginFieldChange = (key, value) => { const error = formPluginState.plugin.parameters.find((parameter) => parameter.name === key).required && value === ''; setFormPluginState({ fields: Object.assign(Object.assign({}, formPluginState.fields), { [key]: value }), error: Object.assign(Object.assign({}, formPluginState.error), { [key]: error }) }); }; const onPluginDetailsSelected = (plugin) => { if (plugin.parameters.length) { setFormPluginState({ plugin, submitted: false, fields: {} }); onPluginDetailsClose(); } else { onInstallPlugin(plugin); } }; const onPluginFormClose = () => { setFormPluginState({ plugin: null, submitted: false, fields: {} }); }; const onPageChange = (page) => { setOffset(page * limit); }; const onRowsPerPageChange = (e) => { setLimit(e.target.value); }; useEffect(() => { if (formPluginState.plugin) { const lookup = {}; formPluginState.plugin.parameters.forEach((parameter) => { if (parameter.required) { lookup[parameter.name] = true; } }); setFormPluginState({ error: lookup }); } }, [formPluginState.plugin, setFormPluginState]); return React.createElement( React.Fragment, null, React.createElement(DialogHeader, { title: installPermission ? React.createElement(FormattedMessage, { id: 'InstallPluginDialog.title', defaultMessage: 'Search & install plugin' }) : React.createElement(FormattedMessage, { id: 'words.search', defaultMessage: 'Search' }), onCloseButtonClick: props.onClose, rightActions: [ { icon: { id: '@mui/icons-material/SearchRounded' }, disabled: isFetching === null || plugins === null || Boolean(selectedDetailsPlugin), onClick: onToggleSearchBar } ] }), React.createElement( DialogBody, { style: { minHeight: '60vh', padding: selectedDetailsPlugin || formPluginState.plugin ? 0 : UNDEFINED } }, isFetching ? React.createElement(LoadingState, { styles: { root: { flexGrow: 1, justifyContent: 'center' } } }) : selectedDetailsPlugin ? React.createElement(PluginDetailsView, { plugin: selectedDetailsPlugin, usePermission: installPermission, inUse: Boolean(installedPlugins[selectedDetailsPlugin.id]), useLabel: Boolean(installedPlugins[selectedDetailsPlugin.id]) ? React.createElement(FormattedMessage, { id: 'words.installed', defaultMessage: 'Installed' }) : React.createElement(FormattedMessage, { id: 'words.install', defaultMessage: 'Install' }), beingInstalled: installingLookup[selectedDetailsPlugin.id], onCloseDetails: onPluginDetailsClose, onBlueprintSelected: onPluginDetailsSelected }) : formPluginState.plugin ? React.createElement(PluginParametersForm, { plugin: formPluginState.plugin, submitted: formPluginState.submitted, fields: formPluginState.fields, onPluginFieldChange: onPluginFieldChange, onCancel: onPluginFormClose }) : React.createElement( React.Fragment, null, showSearchBar && React.createElement(SearchBar, { showActionButton: Boolean(keyword), keyword: keyword, onChange: onSearch, autoFocus: true, classes: { root: classes.searchWrapper } }), error ? React.createElement(ApiResponseErrorState, { error: error }) : plugins && React.createElement(PluginList, { plugins: plugins, installPermission: installPermission, installedPlugins: installedPlugins, installingLookup: installingLookup, onPluginDetails: onPluginDetails, onPluginSelected: onPluginDetailsSelected }) ) ), !selectedDetailsPlugin && !error && !isFetching && React.createElement( DialogFooter, null, formPluginState.plugin ? React.createElement( React.Fragment, null, React.createElement( SecondaryButton, { onClick: onPluginFormClose, sx: { mr: 1 } }, React.createElement(FormattedMessage, { id: 'words.cancel', defaultMessage: 'Cancel' }) ), React.createElement( PrimaryButton, { disabled: Object.values(formPluginState.error).some((value) => value) || (formPluginState.plugin && installingLookup[formPluginState.plugin.id]), onClick: () => onInstallPlugin(formPluginState.plugin, formPluginState.fields) }, React.createElement(FormattedMessage, { id: 'words.install', defaultMessage: 'Install' }) ) ) : plugins && React.createElement(Pagination, { rowsPerPageOptions: [6, 9, 15], mode: 'table', count: plugins.total, rowsPerPage: plugins.limit, page: plugins && Math.ceil(plugins.offset / plugins.limit), onPageChange: (e, page) => onPageChange(page), onRowsPerPageChange: onRowsPerPageChange }) ) ); } export default InstallPluginDialogContainer;