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