UNPKG

@craftercms/studio-ui

Version:

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

472 lines (470 loc) 17.8 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 React, { useCallback, useEffect, useMemo, useState } from 'react'; import { fetchLegacyUserActivities } from '../../../services/dashboard'; import useStyles from './styles'; import { getNumOfMenuOptionsForItem, getSystemTypeFromPath, parseLegacyItemToDetailedItem } from '../../../utils/content'; import LegacyDashletCard from '../LegacyDashletCard'; import { FormattedMessage, useIntl } from 'react-intl'; import RecentActivityDashletGridUI from '../LegacyRecentActivityDashletGrid/RecentActivityDashletGridUI'; import { useDispatch, useSelector } from 'react-redux'; import MenuItem from '@mui/material/MenuItem'; import Button from '@mui/material/Button'; import RecentActivityDashletUiSkeleton from '../LegacyRecentActivityDashletGrid/RecentActivityDashletUISkeleton'; import { deleteContentEvent, publishEvent, workflowEvent, contentEvent } from '../../../state/actions/system'; import { getHostToHostBus } from '../../../utils/subjects'; import { filter, map, switchMap } from 'rxjs/operators'; import TextField from '@mui/material/TextField'; import { useLocale } from '../../../hooks/useLocale'; import { useSpreadState } from '../../../hooks/useSpreadState'; import { getStoredDashboardPreferences, setStoredDashboardPreferences } from '../../../utils/state'; import { createPresenceTable } from '../../../utils/array'; import { showItemMegaMenu } from '../../../state/actions/dialogs'; import { generateMultipleItemOptions, generateSingleItemOptions, itemActionDispatcher } from '../../../utils/itemActions'; import { useEnv } from '../../../hooks/useEnv'; import ActionsBar from '../../ActionsBar'; import translations from './translations'; import { EmptyState, getEmptyStateStyleSet } from '../../EmptyState/EmptyState'; import { useActiveSite } from '../../../hooks/useActiveSite'; import { fetchItemsByPath } from '../../../services/content'; import useItemsByPath from '../../../hooks/useItemsByPath'; import useFetchSandboxItems from '../../../hooks/useFetchSandboxItems'; import useSelection from '../../../hooks/useSelection'; import { ApiResponseErrorState } from '../../ApiResponseErrorState'; const dashletInitialPreferences = { filterBy: 'page', numItems: 10, expanded: true, excludeLiveItems: false }; const actionsToBeShown = [ 'edit', 'delete', 'publish', 'rejectPublish', 'duplicate', 'duplicateAsset', 'dependencies', 'history' ]; export function RecentActivityDashlet() { var _a; const [fetchingActivity, setFetchingActivity] = useState(false); const [errorActivity, setErrorActivity] = useState(); const [items, setItems] = useState([]); const [totalItems, setTotalItems] = useState(0); const { id: siteId, uuid } = useActiveSite(); const currentUser = useSelector((state) => state.user.username); const dashletPreferencesId = 'recentActivityDashlet'; const [preferences, setPreferences] = useSpreadState( (_a = getStoredDashboardPreferences(currentUser, uuid, dashletPreferencesId)) !== null && _a !== void 0 ? _a : dashletInitialPreferences ); const [selectedLookup, setSelectedLookup] = useState({}); const [sortType, setSortType] = useState('desc'); const [sortBy, setSortBy] = useState('dateModified'); const locale = useLocale(); const { classes } = useStyles(); const dispatch = useDispatch(); const { formatMessage } = useIntl(); const { authoringBase } = useEnv(); const itemsByPath = useItemsByPath(); useFetchSandboxItems(Object.keys(selectedLookup)); const itemsBeingFetchedByPath = useSelection((state) => state.content.itemsBeingFetchedByPath); const isFetching = useMemo(() => { return Object.keys(selectedLookup).some((path) => itemsBeingFetchedByPath[path]); }, [itemsBeingFetchedByPath, selectedLookup]); const isAllChecked = useMemo(() => { const nonDeletedItems = items.filter((item) => !item.stateMap.deleted); if (nonDeletedItems.length) { // Is there at least one (non deleted item) that's not checked? If so, they're NOT all checked. return !nonDeletedItems.some((item) => !selectedLookup[item.path]); } else { return false; } }, [items, selectedLookup]); const isIndeterminate = useMemo( () => items.some((item) => selectedLookup[item.path] && !isAllChecked), [items, selectedLookup, isAllChecked] ); const selectedItemsLength = useMemo(() => Object.values(selectedLookup).filter(Boolean).length, [selectedLookup]); const onFilterChange = (e) => { setPreferences({ filterBy: e.target.value }); }; const onNumItemsChange = (e) => { setPreferences({ numItems: e.target.value }); }; useEffect(() => { setStoredDashboardPreferences(preferences, currentUser, uuid, dashletPreferencesId); }, [preferences, currentUser, uuid]); const onToggleHideLiveItems = (e) => { e.stopPropagation(); setPreferences({ excludeLiveItems: !preferences.excludeLiveItems }); }; const toggleSortType = () => { setSortType(sortType === 'asc' ? 'desc' : 'asc'); }; const fetchActivity = useCallback( (backgroundRefresh) => { if (!backgroundRefresh) { setFetchingActivity(true); } fetchLegacyUserActivities( siteId, currentUser, 'eventDate', true, preferences.numItems, preferences.filterBy, preferences.excludeLiveItems ) .pipe( switchMap((activities) => { const paths = []; const pathsToFetch = []; const deleted = {}; const legacyItems = {}; activities.documents.forEach((item) => { let legacyToDetailedParsedItem = parseLegacyItemToDetailedItem(item); let path = legacyToDetailedParsedItem.path; legacyItems[path] = legacyToDetailedParsedItem; paths.push(path); if (item.isDeleted) { deleted[path] = legacyToDetailedParsedItem; } else { pathsToFetch.push(path); } }); return fetchItemsByPath(siteId, pathsToFetch, { castAsDetailedItem: true }).pipe( map((items) => { // The idea is to present the items in the same order that the original call returned. const itemLookup = items.reduce((lookup, item) => { lookup[item.path] = item; return lookup; }, {}); return { total: activities.total, items: paths.map((path) => { var _a, _b, _c, _d; return Object.assign( Object.assign( {}, (_b = (_a = itemLookup[path]) !== null && _a !== void 0 ? _a : deleted[path]) !== null && _b !== void 0 ? _b : legacyItems[path] ), { live: ((_c = itemLookup[path]) === null || _c === void 0 ? void 0 : _c.stateMap.live) ? legacyItems[path].live : null, staging: ((_d = itemLookup[path]) === null || _d === void 0 ? void 0 : _d.stateMap.staged) ? legacyItems[path].staging : null } ); }) }; }) ); }) ) .subscribe({ next(response) { setTotalItems(response.total); setItems(response.items); if (!backgroundRefresh) { setFetchingActivity(false); } }, error(e) { setErrorActivity(e); if (!backgroundRefresh) { setFetchingActivity(false); } } }); }, [siteId, currentUser, preferences.numItems, preferences.filterBy, preferences.excludeLiveItems] ); useEffect(() => { fetchActivity(); }, [fetchActivity]); // region Item Updates Propagation useEffect(() => { const events = [deleteContentEvent.type, workflowEvent.type, publishEvent.type, contentEvent.type]; const hostToHost$ = getHostToHostBus(); const subscription = hostToHost$.pipe(filter((e) => events.includes(e.type))).subscribe(({ type, payload }) => { if (type === deleteContentEvent.type) { setSelectedLookup({}); } fetchActivity(true); }); return () => { subscription.unsubscribe(); }; }, [fetchActivity, selectedLookup]); // endregion const onToggleCheckedAll = () => { if (isAllChecked) { setSelectedLookup({}); } else { setSelectedLookup( Object.assign( Object.assign({}, selectedLookup), createPresenceTable( items.filter((item) => !item.stateMap.deleted), true, (item) => item.path ) ) ); } }; const handleItemChecked = (path) => { setSelectedLookup(Object.assign(Object.assign({}, selectedLookup), { [path]: !selectedLookup[path] })); }; const onItemMenuClick = (event, item) => { const path = item.path; dispatch( showItemMegaMenu({ path, anchorReference: 'anchorPosition', anchorPosition: { top: event.clientY, left: event.clientX }, numOfLoaderItems: getNumOfMenuOptionsForItem({ path: item.path, systemType: getSystemTypeFromPath(item.path) }) }) ); }; const onActionBarOptionClicked = (option) => { if (option === 'clear') { setSelectedLookup({}); } else { const selected = items.filter((item) => selectedLookup[item.path]); itemActionDispatcher({ site: siteId, item: selected.length > 1 ? selected : selected[0], option: option, authoringBase, dispatch, formatMessage }); } }; const selectionOptions = useMemo(() => { const selected = Object.keys(selectedLookup).filter((path) => selectedLookup[path]); if (selected.length === 0) { return null; } else if (selected.length) { if (selected.length === 1) { const path = selected[0]; const item = itemsByPath[path]; return generateSingleItemOptions(item, formatMessage, { includeOnly: actionsToBeShown }).flat(); } else { let items = []; selected.forEach((itemPath) => { const item = itemsByPath[itemPath]; if (item) { items.push(item); } }); return generateMultipleItemOptions(items, formatMessage, { includeOnly: actionsToBeShown }); } } }, [formatMessage, itemsByPath, selectedLookup]); return React.createElement( LegacyDashletCard, { title: React.createElement( React.Fragment, null, React.createElement(FormattedMessage, { id: 'recentActivityDashlet.dashletTitle', defaultMessage: 'My Recent Activity' }), ' (', items.length, ')' ), onToggleExpanded: () => setPreferences({ expanded: !preferences.expanded }), expanded: preferences.expanded, refreshDisabled: fetchingActivity, onRefresh: fetchActivity, headerRightSection: React.createElement( React.Fragment, null, React.createElement( Button, { onClick: onToggleHideLiveItems, className: classes.rightAction }, preferences.excludeLiveItems ? React.createElement(FormattedMessage, { id: 'recentActivityDashlet.showLiveItems', defaultMessage: 'Show Live Items' }) : React.createElement(FormattedMessage, { id: 'recentActivityDashlet.hideLiveItems', defaultMessage: 'Hide Live Items' }) ), React.createElement( TextField, { label: React.createElement(FormattedMessage, { id: 'words.show', defaultMessage: 'Show' }), select: true, size: 'small', value: preferences.numItems, disabled: fetchingActivity, onChange: onNumItemsChange, onClick: (e) => e.stopPropagation(), className: classes.rightAction }, React.createElement(MenuItem, { value: 10 }, '10'), React.createElement(MenuItem, { value: 20 }, '20'), React.createElement(MenuItem, { value: 50 }, '50'), totalItems > 0 && React.createElement( MenuItem, { value: totalItems }, React.createElement(FormattedMessage, { id: 'words.all', defaultMessage: 'All' }), ' (', totalItems, ')' ) ), React.createElement( TextField, { label: React.createElement(FormattedMessage, { id: 'recentActivityDashlet.filterBy', defaultMessage: 'Filter by' }), select: true, size: 'small', value: preferences.filterBy, disabled: fetchingActivity, onChange: onFilterChange, onClick: (e) => e.stopPropagation() }, React.createElement( MenuItem, { value: 'page' }, React.createElement(FormattedMessage, { id: 'words.pages', defaultMessage: 'Pages' }) ), React.createElement( MenuItem, { value: 'component' }, React.createElement(FormattedMessage, { id: 'words.components', defaultMessage: 'Components' }) ), React.createElement( MenuItem, { value: 'asset' }, React.createElement(FormattedMessage, { id: 'words.assets', defaultMessage: 'Assets' }) ), React.createElement( MenuItem, { value: 'all' }, React.createElement(FormattedMessage, { id: 'words.all', defaultMessage: 'All' }) ) ) ) }, errorActivity ? React.createElement(ApiResponseErrorState, { error: errorActivity }) : fetchingActivity ? React.createElement(RecentActivityDashletUiSkeleton, { numOfItems: items.length }) : items ? items.length ? React.createElement( React.Fragment, null, (isIndeterminate || isAllChecked) && React.createElement(ActionsBar, { classes: { root: classes.actionsBarRoot, checkbox: classes.actionsBarCheckbox }, options: selectionOptions === null || selectionOptions === void 0 ? void 0 : selectionOptions.concat([ { id: 'clear', label: formatMessage(translations.clear, { count: selectedItemsLength }) } ]), isIndeterminate: isIndeterminate, isChecked: isAllChecked, isLoading: isFetching, numOfSkeletonItems: selectedItemsLength > 1 ? 3 : 7, onOptionClicked: onActionBarOptionClicked, onCheckboxChange: onToggleCheckedAll }), React.createElement(RecentActivityDashletGridUI, { items: items, onOptionsButtonClick: onItemMenuClick, selectedLookup: selectedLookup, isAllChecked: isAllChecked, isIndeterminate: isIndeterminate, locale: locale, sortType: sortType, toggleSortType: toggleSortType, sortBy: sortBy, setSortBy: setSortBy, onItemChecked: handleItemChecked, onClickSelectAll: onToggleCheckedAll }) ) : React.createElement(EmptyState, { title: React.createElement(FormattedMessage, { id: 'recentActivityDashlet.emptyMessage', defaultMessage: 'No recent activity' }), styles: Object.assign( Object.assign({}, getEmptyStateStyleSet('horizontal')), getEmptyStateStyleSet('image-sm') ) }) : React.createElement(React.Fragment, null) ); } export default RecentActivityDashlet;