@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
482 lines (480 loc) • 16.6 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, { useCallback, useEffect, useState } from 'react';
import { makeStyles } from 'tss-react/mui';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import { defineMessages, useIntl } from 'react-intl';
import PublishingPackage from './PublishingPackage';
import { cancelPackage, fetchPackages, fetchPublishingTargets } from '../../services/publishing';
import FilterDropdown from './FilterDropdown';
import { setRequestForgeryToken } from '../../utils/auth';
import TablePagination from '@mui/material/TablePagination';
import EmptyState from '../EmptyState/EmptyState';
import Typography from '@mui/material/Typography';
import HighlightOffIcon from '@mui/icons-material/HighlightOffRounded';
import RefreshIcon from '@mui/icons-material/RefreshRounded';
import Button from '@mui/material/Button';
import { alpha } from '@mui/material/styles';
import { BLOCKED, CANCELLED, COMPLETED, PROCESSING, READY_FOR_LIVE } from './constants';
import palette from '../../styles/palette';
import ApiResponseErrorState from '../ApiResponseErrorState';
import { useSpreadState } from '../../hooks/useSpreadState';
import ConfirmDropdown from '../ConfirmDropdown';
import { publishEvent, workflowEvent } from '../../state/actions/system';
import { getHostToHostBus } from '../../utils/subjects';
import { filter } from 'rxjs/operators';
import { LoadingState } from '../LoadingState';
const messages = defineMessages({
selectAll: {
id: 'publishingDashboard.selectAll',
defaultMessage: 'Select all on this page'
},
cancelSelected: {
id: 'publishingDashboard.cancelSelected',
defaultMessage: 'Cancel Selected'
},
cancel: {
id: 'publishingDashboard.no',
defaultMessage: 'No'
},
confirm: {
id: 'publishingDashboard.yes',
defaultMessage: 'Yes'
},
confirmAllHelper: {
id: 'publishingDashboard.confirmAllHelper',
defaultMessage: 'Set the state for all selected items to "Cancelled"?'
},
filters: {
id: 'publishingDashboard.filters',
defaultMessage: 'Filters'
},
noPackagesTitle: {
id: 'publishingDashboard.noPackagesTitle',
defaultMessage: 'No packages were found'
},
noPackagesSubtitle: {
id: 'publishingDashboard.noPackagesSubtitle',
defaultMessage: 'Try changing your query'
},
filteredBy: {
id: 'publishingDashboard.filteredBy',
defaultMessage:
'Showing: {state, select, all {} other {Status: {state}.}} {environment, select, all {} other {{environment} target.}} {path, select, none {} other {Filtered by {path}}}'
},
packagesSelected: {
id: 'publishingDashboard.packagesSelected',
defaultMessage: '{count, plural, one {{count} Package selected} other {{count} Packages selected}}'
},
previous: {
id: 'publishingDashboard.previous',
defaultMessage: 'Previous page'
},
next: {
id: 'publishingDashboard.next',
defaultMessage: 'Next page'
}
});
const useStyles = makeStyles()((theme) => ({
publishingQueue: {},
topBar: {
display: 'flex',
padding: '0 0 0 0',
alignItems: 'center',
borderBottom: '1px solid #dedede',
justifyContent: 'flex-end'
},
secondBar: {
background: theme.palette.divider,
padding: '10px',
borderBottom: '1px solid #dedede'
},
queueList: {},
package: {
padding: '20px',
'& .name': {
display: 'flex',
justifyContent: 'space-between'
},
'& .status': {
display: 'flex',
justifyContent: 'space-between'
},
'& .comment': {
display: 'flex',
justifyContent: 'space-between',
'& div:first-child': {
marginRight: '20px'
}
},
'& .files': {}
},
packagesSelected: {
marginRight: '10px',
display: 'flex',
alignItems: 'center'
},
clearSelected: {
marginLeft: '5px',
cursor: 'pointer'
},
selectAll: {
marginRight: 'auto'
},
button: {
margin: theme.spacing(1)
},
empty: {
padding: '40px 0'
},
cancelButton: {
paddingRight: '10px',
color: palette.orange.main,
border: `1px solid ${alpha(palette.orange.main, 0.5)}`,
'&:hover': {
backgroundColor: alpha(palette.orange.main, 0.08)
}
}
}));
const currentFiltersInitialState = {
environment: '',
path: '',
state: [READY_FOR_LIVE, PROCESSING, COMPLETED, CANCELLED, BLOCKED],
limit: 5,
page: 0
};
function getFilters(currentFilters) {
let filters = {};
if (currentFilters.environment) filters['environment'] = currentFilters.environment;
if (currentFilters.path) filters['path'] = currentFilters.path;
if (currentFilters.state.length) filters['states'] = currentFilters.state;
if (currentFilters.limit) filters['limit'] = currentFilters.limit;
if (currentFilters.page) filters['offset'] = currentFilters.page * currentFilters.limit;
return filters;
}
function renderCount(selected) {
let _selected = [];
Object.keys(selected).forEach((key) => {
if (selected[key]) {
_selected.push(key);
}
});
return _selected;
}
function PublishingQueue(props) {
const { classes } = useStyles();
const [packages, setPackages] = useState(null);
const [isFetchingPackages, setIsFetchingPackages] = useState(false);
const [filesPerPackage, setFilesPerPackage] = useState(null);
const [selected, setSelected] = useState({});
const [pending, setPending] = useState({});
const [count, setCount] = useState(0);
const [total, setTotal] = useState(0);
const [filters, setFilters] = useSpreadState({
environments: null,
states: currentFiltersInitialState.state
});
const [apiState, setApiState] = useSpreadState({
error: false,
errorResponse: null
});
const [currentFilters, setCurrentFilters] = useState(currentFiltersInitialState);
const { formatMessage } = useIntl();
const { siteId, readOnly } = props;
const hasReadyForLivePackages = (packages || []).filter((item) => item.state === READY_FOR_LIVE).length > 0;
const getPackages = useCallback(
(siteId) => {
setIsFetchingPackages(true);
if (currentFilters.state.length) {
fetchPackages(siteId, getFilters(currentFilters)).subscribe({
next: (packages) => {
setIsFetchingPackages(false);
setTotal(packages.total);
setPackages(packages);
},
error: ({ response }) => {
setIsFetchingPackages(false);
setApiState({ error: true, errorResponse: response.response });
}
});
}
},
[currentFilters, setApiState]
);
setRequestForgeryToken();
useEffect(() => {
fetchPublishingTargets(siteId).subscribe({
next({ publishingTargets: targets }) {
let channels = [];
targets.forEach((channel) => {
channels.push(channel.name);
});
setFilters({ environments: channels });
},
error({ response }) {
setApiState({ error: true, errorResponse: response });
}
});
}, [siteId, setFilters, setApiState]);
useEffect(() => {
getPackages(siteId);
}, [currentFilters, siteId, getPackages]);
useEffect(() => {
setCount(renderCount(selected).length);
}, [selected]);
useEffect(() => {
const events = [workflowEvent.type, publishEvent.type];
const hostToHost$ = getHostToHostBus();
const subscription = hostToHost$.pipe(filter((e) => events.includes(e.type))).subscribe(({ type, payload }) => {
getPackages(siteId);
});
return () => {
subscription.unsubscribe();
};
}, [siteId, getPackages]);
function renderPackages() {
return packages.map((item, index) =>
React.createElement(PublishingPackage, {
id: item.id,
approver: item.approver,
schedule: item.schedule,
state: item.state,
comment: item.comment,
environment: item.environment,
key: index,
siteId: siteId,
selected: selected,
pending: pending,
setPending: setPending,
getPackages: getPackages,
setApiState: setApiState,
setSelected: setSelected,
filesPerPackage: filesPerPackage,
setFilesPerPackage: setFilesPerPackage,
readOnly: readOnly
})
);
}
function handleCancelAll() {
if (count === 0) return false;
let _pending = {};
Object.keys(selected).forEach((key) => {
if (selected[key]) {
_pending[key] = true;
}
});
setPending(_pending);
cancelPackage(siteId, Object.keys(_pending)).subscribe({
next() {
Object.keys(selected).forEach((key) => {
_pending[key] = false;
});
setPending(Object.assign(Object.assign({}, pending), _pending));
clearSelected();
getPackages(siteId);
},
error({ response }) {
setApiState({ error: true, errorResponse: response });
}
});
}
function clearSelected() {
setSelected({});
}
function handleSelectAll(event) {
if (!packages || packages.length === 0) return false;
let _selected = {};
if (event.target.checked) {
packages.forEach((item) => {
_selected[item.id] = item.state === READY_FOR_LIVE;
setSelected(Object.assign(Object.assign({}, selected), _selected));
});
} else {
packages.forEach((item) => {
_selected[item.id] = false;
setSelected(Object.assign(Object.assign({}, selected), _selected));
});
}
}
function areAllSelected() {
if ((packages === null || packages === void 0 ? void 0 : packages.length) === 0 || !hasReadyForLivePackages) {
return false;
} else {
return !packages.some(
(item) =>
// There is at least one that is not selected
item.state === READY_FOR_LIVE && !selected[item.id]
);
}
}
function handleFilterChange(event) {
if (event.target.type === 'radio') {
clearSelected();
setCurrentFilters(
Object.assign(Object.assign({}, currentFilters), { [event.target.name]: event.target.value, page: 0 })
);
} else if (event.target.type === 'checkbox') {
let state = [...currentFilters.state];
if (event.target.checked) {
if (event.target.value) {
state.push(event.target.value);
} else {
state = [...filters.states];
}
} else {
if (event.target.value) {
state.splice(state.indexOf(event.target.value), 1);
} else {
state = [];
}
}
setCurrentFilters(Object.assign(Object.assign({}, currentFilters), { state, page: 0 }));
}
}
function handleEnterKey(path) {
setCurrentFilters(Object.assign(Object.assign({}, currentFilters), { path: path, page: 0 }));
}
function handleChangePage(event, newPage) {
setCurrentFilters(Object.assign(Object.assign({}, currentFilters), { page: newPage }));
}
function handleChangeRowsPerPage(event) {
setCurrentFilters(
Object.assign(Object.assign({}, currentFilters), { page: 0, limit: parseInt(event.target.value, 10) })
);
}
return React.createElement(
'div',
{ className: classes.publishingQueue },
React.createElement(
'div',
{ className: classes.topBar },
currentFilters.state.includes(READY_FOR_LIVE) &&
React.createElement(
FormGroup,
{ className: classes.selectAll },
React.createElement(FormControlLabel, {
control: React.createElement(Checkbox, {
color: 'primary',
checked: areAllSelected(),
disabled: !packages || !hasReadyForLivePackages || readOnly,
onClick: handleSelectAll
}),
label: formatMessage(messages.selectAll)
})
),
count > 0 &&
currentFilters.state.includes(READY_FOR_LIVE) &&
React.createElement(
Typography,
{ variant: 'body2', className: classes.packagesSelected, color: 'textSecondary' },
formatMessage(messages.packagesSelected, { count: count }),
React.createElement(HighlightOffIcon, { className: classes.clearSelected, onClick: clearSelected })
),
React.createElement(
Button,
{ variant: 'outlined', className: classes.button, onClick: () => getPackages(siteId) },
React.createElement(RefreshIcon, null)
),
currentFilters.state.includes(READY_FOR_LIVE) &&
React.createElement(ConfirmDropdown, {
classes: { button: classes.cancelButton },
text: formatMessage(messages.cancelSelected),
cancelText: formatMessage(messages.cancel),
confirmText: formatMessage(messages.confirm),
confirmHelperText: formatMessage(messages.confirmAllHelper),
onConfirm: handleCancelAll,
disabled: !(hasReadyForLivePackages && Object.values(selected).length > 0) || readOnly
}),
React.createElement(FilterDropdown, {
className: classes.button,
text: formatMessage(messages.filters),
handleFilterChange: handleFilterChange,
currentFilters: currentFilters,
handleEnterKey: handleEnterKey,
filters: filters
})
),
(currentFilters.state.length || currentFilters.path || currentFilters.environment) &&
React.createElement(
'div',
{ className: classes.secondBar },
React.createElement(
Typography,
{ variant: 'body2' },
formatMessage(messages.filteredBy, {
state: currentFilters.state
? React.createElement('strong', { key: 'state' }, currentFilters.state.join(', '))
: 'all',
path: currentFilters.path ? React.createElement('strong', { key: 'path' }, currentFilters.path) : 'none',
environment: currentFilters.environment
? React.createElement('strong', { key: 'environment' }, currentFilters.environment)
: 'all'
})
)
),
apiState.error && apiState.errorResponse
? React.createElement(ApiResponseErrorState, { error: apiState.errorResponse })
: React.createElement(
'div',
{ className: classes.queueList },
packages === null && isFetchingPackages && React.createElement(LoadingState, null),
packages && renderPackages(),
packages !== null &&
packages.length === 0 &&
React.createElement(
'div',
{ className: classes.empty },
React.createElement(EmptyState, {
title: formatMessage(messages.noPackagesTitle),
subtitle: formatMessage(messages.noPackagesSubtitle)
})
)
),
React.createElement(TablePagination, {
rowsPerPageOptions: [3, 5, 10],
component: 'div',
count: total,
rowsPerPage: currentFilters.limit,
page: currentFilters.page,
backIconButtonProps: {
'aria-label': formatMessage(messages.previous)
},
nextIconButtonProps: {
'aria-label': formatMessage(messages.next)
},
onPageChange: handleChangePage,
onRowsPerPageChange: handleChangeRowsPerPage
})
);
}
export default PublishingQueue;