UNPKG

@craftercms/studio-ui

Version:

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

374 lines (372 loc) 15.2 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 { useSpreadState } from '../../hooks/useSpreadState'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { paths } from './utils'; import { useActiveSiteId } from '../../hooks/useActiveSiteId'; import { useDispatch } from 'react-redux'; import { fetchPublishingTargets } from '../../services/publishing'; import { getComputedPublishingTarget, getDateScheduled } from '../../utils/content'; import { FormattedMessage } from 'react-intl'; import { useLogicResource } from '../../hooks/useLogicResource'; import { createPresenceTable } from '../../utils/array'; import { fetchDependencies } from '../../services/dependencies'; import { PublishDialogUI } from './PublishDialogUI'; import useStyles from './styles'; import { useSelection } from '../../hooks/useSelection'; import { isBlank } from '../../utils/string'; import { useLocale } from '../../hooks/useLocale'; import { getUserTimeZone } from '../../utils/datetime'; import { pluckProps } from '../../utils/object'; import moment from 'moment-timezone'; import { updatePublishDialog } from '../../state/actions/dialogs'; import { approve, publish, requestPublish } from '../../services/workflow'; import useDetailedItems from '../../hooks/useDetailedItems'; export function PublishDialogContainer(props) { const { items, scheduling = 'now', onSuccess, onClose, isSubmitting } = props; const detailedItems = useDetailedItems(items.map((item) => item.path)); const { dateTimeFormatOptions: { timeZone = getUserTimeZone() } } = useLocale(); const [state, setState] = useSpreadState({ emailOnApprove: false, requestApproval: false, publishingTarget: '', submissionComment: '', scheduling, scheduledDateTime: ((date) => { date.setSeconds(0); return moment(date).tz(timeZone).format(); })(new Date()), publishingChannel: null, scheduledTimeZone: timeZone, error: null, fetchingDependencies: false }); const [published, setPublished] = useState(null); const [publishingTargets, setPublishingTargets] = useState(null); const [publishingTargetsStatus, setPublishingTargetsStatus] = useState('Loading'); const [selectedItems, setSelectedItems] = useState({}); const [dependencies, setDependencies] = useState(null); const [submitDisabled, setSubmitDisabled] = useState(true); const siteId = useActiveSiteId(); const hasPublishPermission = !(items === null || items === void 0 ? void 0 : items.some((item) => !item.availableActionsMap.publish)); const dispatch = useDispatch(); const submissionCommentRequired = useSelection((state) => state.uiConfig.publishing.publishCommentRequired); const isApprove = hasPublishPermission && items.every((item) => item.stateMap.submitted); const submit = !hasPublishPermission || state.requestApproval ? requestPublish : isApprove ? approve : publish; const { mixedPublishingTargets, mixedPublishingDates, dateScheduled, publishingTarget } = useMemo(() => { const state = { mixedPublishingTargets: false, mixedPublishingDates: false, dateScheduled: null, publishingTarget: null }; let itemsChecked = items.filter((item) => selectedItems[item.path]); if (itemsChecked.length === 0) { state.publishingTarget = ''; return state; } // region Discover mixed targets and/or schedules and sets the publishingTarget based off the items let target; let schedule; itemsChecked.some((item, index) => { const computedTarget = getComputedPublishingTarget(itemsChecked[0]); const computedSchedule = getDateScheduled(itemsChecked[0]); if (index === 0) { target = computedTarget; schedule = computedSchedule; } else { if (target !== computedTarget) { // If the computed target is different, we have mixed targets. // Could be any combination of live vs staging vs null that triggers mixed targets. state.mixedPublishingTargets = true; } if (schedule !== computedSchedule) { // If the current item's computed scheduled date is different, we have mixed dates. // Could be any combination of live vs staging vs null that triggers mixed targets. state.mixedPublishingDates = true; } } if (state.publishingTarget === null && computedTarget !== null) { state.publishingTarget = computedTarget; } // First found dateScheduled cached for later if (state.dateScheduled === null && computedSchedule !== null) { state.dateScheduled = computedSchedule; } // Once these things are found to be true, no need to iterate further. return state.mixedPublishingTargets && state.mixedPublishingDates && state.dateScheduled !== null; }); // endregion // If there aren't any available target (or they haven't loaded), dialog should not have a selected target. if (publishingTargets === null || publishingTargets === void 0 ? void 0 : publishingTargets.length) { // If there are mixed targets, we want manual user selection of a target. // Otherwise, use what was previously found as the target on the selected items. if (state.mixedPublishingTargets) { state.publishingTarget = ''; } else { // If we haven't found a target by this point, we wish to default the dialog to // staging (as long as that target is enabled in the system, which is checked next). if (state.publishingTarget === null) { state.publishingTarget = 'staging'; } state.publishingTarget = publishingTargets.some((target) => target.name === state.publishingTarget) ? state.publishingTarget : publishingTargets[0].name; } } else { state.publishingTarget = ''; } return state; }, [selectedItems, items, publishingTargets]); const getPublishingChannels = useCallback( (success, error) => { setPublishingTargetsStatus('Loading'); fetchPublishingTargets(siteId).subscribe({ next({ publishingTargets: targets, published }) { setPublished(published); setPublishingTargets(targets); setPublishingTargetsStatus('Success'); success === null || success === void 0 ? void 0 : success(targets); }, error(e) { setPublishingTargetsStatus('Error'); error === null || error === void 0 ? void 0 : error(e); } }); }, [siteId] ); const publishSource = useMemo( () => ({ items: !detailedItems.isFetching ? Object.values(detailedItems.itemsByPath) : null, error: state.error, submitting: isSubmitting, publishingTargets }), [detailedItems.isFetching, detailedItems.itemsByPath, state.error, isSubmitting, publishingTargets] ); const resource = useLogicResource(publishSource, { shouldResolve: (source) => Boolean(source.items && source.publishingTargets), shouldReject: (source) => Boolean(source.error), shouldRenew: (source, resource) => source.submitting && resource.complete, resultSelector: (source) => pluckProps(source, 'items', 'publishingTargets'), errorSelector: (source) => source.error }); useEffect(() => { getPublishingChannels(() => { setSelectedItems(createPresenceTable(items, true, (item) => item.path)); }); }, [getPublishingChannels, items]); useEffect(() => { setState({ scheduling }); }, [scheduling, setState]); useEffect(() => { if (dateScheduled && scheduling !== 'now') { setState({ scheduling: 'custom', publishingTarget, scheduledDateTime: dateScheduled }); } else if (dateScheduled === null && scheduling === null) { setState({ scheduling: 'now', publishingTarget }); } else { setState({ publishingTarget }); } }, [dateScheduled, publishingTarget, setState, scheduling]); useEffect(() => { // Submit button should be disabled: setSubmitDisabled( // While submitting isSubmitting || // When no items are selected !Object.values(selectedItems).filter(Boolean).length || // When there are no available/loaded publishing targets !(publishingTargets === null || publishingTargets === void 0 ? void 0 : publishingTargets.length) || // When no publishing target is selected !state.publishingTarget || // If submission comment is required (per config) and blank (submissionCommentRequired && isBlank(state.submissionComment)) ); }, [ isSubmitting, selectedItems, publishingTargets, state.publishingTarget, state.submissionComment, submissionCommentRequired ]); const handleSubmit = () => { const { publishingTarget, scheduling: schedule, emailOnApprove: sendEmail, submissionComment, scheduledDateTime: scheduledDate } = state; const items = Object.entries(selectedItems) .filter(([, isChecked]) => isChecked) .map(([path]) => path); const data = Object.assign( { publishingTarget, items, sendEmailNotifications: sendEmail, comment: submissionComment }, schedule === 'custom' ? { schedule: scheduledDate } : {} ); dispatch(updatePublishDialog({ isSubmitting: true })); submit(siteId, data).subscribe( () => { dispatch(updatePublishDialog({ isSubmitting: false, hasPendingChanges: false })); onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({ schedule: schedule, publishingTarget, // @ts-ignore - TODO: Not quite sure if users of this dialog are making use of the `environment` prop name. Should use `publishingTarget` instead. environment: publishingTarget, type: !hasPublishPermission || state.requestApproval ? 'submit' : 'publish', items: items.map((path) => props.items.find((item) => item.path === path)) }); }, (error) => { dispatch(updatePublishDialog({ isSubmitting: false })); } ); }; const onItemClicked = (e, path) => { e.stopPropagation(); e.preventDefault(); setSelectedItems(Object.assign(Object.assign({}, selectedItems), { [path]: !selectedItems[path] })); }; const onSelectAll = () => { setSelectedItems( items.reduce((checked, item) => { checked[item.path] = true; return checked; }, Object.assign({}, selectedItems)) ); }; function onSelectAllSoft() { // If one that is not checked is found, check all. Otherwise, uncheck all. const check = Boolean(dependencies.softDependencies.find((path) => !selectedItems[path])); setSelectedItems( dependencies.softDependencies.reduce((nextCheckedSoftDependencies, path) => { nextCheckedSoftDependencies[path] = check; return nextCheckedSoftDependencies; }, Object.assign({}, selectedItems)) ); } function onFetchDependenciesClick() { setState({ fetchingDependencies: true }); fetchDependencies(siteId, paths(selectedItems)).subscribe( (items) => { setState({ fetchingDependencies: false }); setDependencies(items); }, () => { setState({ fetchingDependencies: false }); setDependencies(null); } ); } const onPublishingArgumentChange = (e) => { let value; switch (e.target.type) { case 'checkbox': value = e.target.checked; break; case 'textarea': value = e.target.value; dispatch( updatePublishDialog({ hasPendingChanges: true }) ); break; case 'radio': value = e.target.value; break; case 'dateTimePicker': { // @ts-ignore const changes = e.target.value; value = changes.dateString; setState({ [e.target.name]: value, scheduledTimeZone: changes.timeZoneName }); return; } default: console.error('Publishing argument change event ignored.'); return; } setState({ [e.target.name]: value }); }; const onCloseButtonClick = (e) => onClose(e, null); return React.createElement(PublishDialogUI, { published: published, resource: resource, publishingTargetsStatus: publishingTargetsStatus, onPublishingChannelsFailRetry: getPublishingChannels, onCloseButtonClick: onCloseButtonClick, handleSubmit: handleSubmit, state: state, isSubmitting: isSubmitting, selectedItems: selectedItems, onItemClicked: onItemClicked, dependencies: dependencies, onSelectAll: onSelectAll, onSelectAllSoftDependencies: onSelectAllSoft, onClickShowAllDeps: onFetchDependenciesClick, classes: useStyles().classes, isRequestPublish: !hasPublishPermission || state.requestApproval, showRequestApproval: hasPublishPermission && items.every((item) => !item.stateMap.submitted), submitLabel: state.scheduling === 'custom' ? React.createElement(FormattedMessage, { id: 'words.schedule', defaultMessage: 'Schedule' }) : !hasPublishPermission || state.requestApproval ? React.createElement(FormattedMessage, { id: 'publishDialog.requestPublish', defaultMessage: 'Request Publish' }) : React.createElement(FormattedMessage, { id: 'words.publish', defaultMessage: 'Publish' }), mixedPublishingTargets: mixedPublishingTargets, mixedPublishingDates: mixedPublishingDates, submissionCommentRequired: submissionCommentRequired, submitDisabled: submitDisabled, onPublishingArgumentChange: onPublishingArgumentChange }); } export default PublishDialogContainer;