@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
293 lines (291 loc) • 11.5 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 { 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;