@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
341 lines (339 loc) • 12.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-2023 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 { getCurrentPage, useSelectionOptions } from '../SiteDashboard';
import useActiveSiteId from '../../hooks/useActiveSiteId';
import { FormattedMessage, useIntl } from 'react-intl';
import useEnv from '../../hooks/useEnv';
import { useDispatch } from 'react-redux';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { fetchActivity } from '../../services/dashboard';
import useActiveUser from '../../hooks/useActiveUser';
import { DashletCard } from '../DashletCard';
import RefreshRounded from '@mui/icons-material/RefreshRounded';
import {
DashletEmptyMessage,
DashletItemOptions,
getItemSkeleton,
ListItemIcon,
Pager,
PersonAvatar
} from '../DashletCard/dashletCommons';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import { renderActivity, renderActivityTimestamp } from '../ActivityDashlet';
import useLocale from '../../hooks/useLocale';
import { PREVIEW_URL_PATH, UNDEFINED } from '../../utils/constants';
import { changeCurrentUrl } from '../../state/actions/preview';
import { getSystemLink } from '../../utils/system';
import useSpreadState from '../../hooks/useSpreadState';
import ListItem from '@mui/material/ListItem';
import PackageDetailsDialog from '../PackageDetailsDialog';
import { contentEvent, deleteContentEvent, publishEvent, workflowEvent } from '../../state/actions/system';
import { getHostToHostBus } from '../../utils/subjects';
import { filter } from 'rxjs/operators';
import Checkbox from '@mui/material/Checkbox';
import Box from '@mui/material/Box';
import ActionsBar from '../ActionsBar';
import useItemsByPath from '../../hooks/useItemsByPath';
import useFetchSandboxItems from '../../hooks/useFetchSandboxItems';
import { itemActionDispatcher } from '../../utils/itemActions';
import ListItemButton from '@mui/material/ListItemButton';
import { fetchSandboxItemComplete, fetchSandboxItems } from '../../state/actions/content';
import LoadingIconButton from '../LoadingIconButton';
import { fetchItemByPath } from '../../services/content';
export function MyRecentActivityDashlet(props) {
const { borderLeftColor = 'success.main', onMinimize } = props;
const locale = useLocale();
const site = useActiveSiteId();
const { formatMessage } = useIntl();
const { authoringBase } = useEnv();
const { username, firstName, lastName } = useActiveUser();
const person = { username, firstName, lastName, avatar: null };
const dispatch = useDispatch();
const [
{ loading, loadingSkeleton, total, feed, limit, offset, selectedPackageId, openPackageDetailsDialog },
setState
] = useSpreadState({
feed: null,
loading: false,
loadingSkeleton: true,
total: null,
limit: 50,
offset: 0,
openPackageDetailsDialog: false,
selectedPackageId: null
});
const currentPage = offset / limit;
const totalPages = total ? Math.ceil(total / limit) : 0;
const itemsByPath = useItemsByPath();
const [selectedPaths, setSelectedPaths] = useState([]);
useFetchSandboxItems(selectedPaths);
const selectedItems = useMemo(() => {
const items = [];
if (selectedPaths.length > 0) {
selectedPaths.forEach((path) => {
if (itemsByPath[path]) {
items.push(itemsByPath[path]);
}
});
}
return items;
}, [itemsByPath, selectedPaths]);
const selectedCount = selectedItems.length;
const selectionOptions = useSelectionOptions(selectedItems, formatMessage, selectedCount);
const siteId = useActiveSiteId();
const [loadingActionsBar, setLoadingActionsBar] = useState(false);
const loadPage = useCallback(
(pageNumber, backgroundRefresh) => {
const newOffset = pageNumber * limit;
setState({
loading: true,
loadingSkeleton: !backgroundRefresh
});
fetchActivity(site, {
usernames: [username],
offset: newOffset,
limit
}).subscribe((feed) => {
setState({ feed, total: feed.total, offset: newOffset, loading: false });
});
},
[limit, setState, site, username]
);
const onItemClick = (previewUrl, e) => {
const pathname = window.location.pathname;
if (pathname.includes(PREVIEW_URL_PATH)) {
dispatch(changeCurrentUrl(previewUrl));
onMinimize?.();
} else {
window.location.href = getSystemLink({
page: previewUrl,
systemLinkId: 'preview',
site,
authoringBase
});
}
};
const onPackageClick = (pkg) => {
setState({ openPackageDetailsDialog: true, selectedPackageId: pkg.id });
};
const onRefresh = () => {
loadPage(getCurrentPage(offset, limit), true);
};
const handleSelect = (e, path) => {
e.stopPropagation();
const isSelected = selectedPaths.includes(path);
if (!isSelected) {
// If item has been already fetched, re-fecth to get the latest version, if not loaded, it'll be fetched with the
// useFetchSandboxItems hook
if (itemsByPath[path]) {
setLoadingActionsBar(true);
fetchItemByPath(siteId, path).subscribe((item) => {
dispatch(fetchSandboxItemComplete({ item }));
setLoadingActionsBar(false);
});
}
setSelectedPaths([...selectedPaths, path]);
} else {
let selectedItems = [...selectedPaths];
let index = selectedItems.indexOf(path);
selectedItems.splice(index, 1);
setSelectedPaths(selectedItems);
}
};
const onOptionClicked = (option) => {
if (option === 'clear') {
setSelectedPaths([]);
} else {
return itemActionDispatcher({
site,
authoringBase,
dispatch,
formatMessage,
option,
item: selectedItems.length > 1 ? selectedItems : selectedItems[0]
});
}
};
useEffect(() => {
loadPage(0);
}, [loadPage]);
// 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 }) => {
dispatch(fetchSandboxItems({ paths: selectedPaths }));
loadPage(getCurrentPage(offset, limit), true);
});
return () => {
subscription.unsubscribe();
};
}, [limit, offset, loadPage, username, dispatch, selectedPaths]);
// endregion
return React.createElement(
DashletCard,
{
...props,
borderLeftColor: borderLeftColor,
title: React.createElement(
React.Fragment,
null,
person &&
React.createElement(PersonAvatar, {
person: person,
sx: {
display: 'inline-flex',
mr: 1,
width: 30,
height: 30,
fontSize: '1.1rem'
}
}),
React.createElement(FormattedMessage, { defaultMessage: 'My Activity' })
),
sxs: {
actionsBar: { padding: 0 },
content: { padding: 0 },
footer: { justifyContent: 'space-between' }
},
headerAction: React.createElement(
LoadingIconButton,
{ onClick: onRefresh, loading: loading },
React.createElement(RefreshRounded, null)
),
footer:
Boolean(feed?.length) &&
React.createElement(Pager, {
totalPages: totalPages,
totalItems: total,
currentPage: currentPage,
rowsPerPage: limit,
onPagePickerChange: (page) => loadPage(page),
onPageChange: (page) => loadPage(page),
onRowsPerPageChange: (rowsPerPage) => setState({ limit: rowsPerPage })
}),
actionsBar: React.createElement(ActionsBar, {
disabled: loading,
isLoading: loadingActionsBar,
isChecked: false,
isIndeterminate: false,
onCheckboxChange: null,
onOptionClicked: onOptionClicked,
options: selectionOptions?.concat([
...(selectedCount > 0
? [
{
id: 'clear',
label: formatMessage(
{
defaultMessage: 'Clear {count} selected'
},
{ count: selectedCount }
)
}
]
: [])
]),
buttonProps: { size: 'small' },
showCheckbox: false,
sxs: {
root: { flexGrow: 1 },
container: {
bgcolor: selectedCount > 0 ? 'action.selected' : UNDEFINED,
minHeight: 33,
paddingLeft: '5px'
},
checkbox: { padding: '5px', borderRadius: 0 },
button: { minWidth: 50 }
}
})
},
loading && loadingSkeleton && getItemSkeleton({ numOfItems: 3, showAvatar: false, showCheckbox: true }),
feed &&
React.createElement(
List,
{ sx: { pb: 0 } },
feed.map((activity) => {
const isItemActivity = activity.item && activity.item.systemType;
const ListItemComponent = isItemActivity ? ListItemButton : ListItem;
const listItemComponentProps = isItemActivity ? { onClick: (e) => handleSelect(e, activity.item.path) } : {};
return (
// Property 'button' is missing in type showing when conditionally rendering ListItemButton or ListItem
// and not showing when using ListItemButton or ListItem directly.
// @ts-ignore
React.createElement(
ListItemComponent,
{ key: activity.id, sx: { pt: 0, pb: 0 }, ...listItemComponentProps },
React.createElement(
ListItemIcon,
null,
activity.item && activity.item.systemType
? React.createElement(Checkbox, {
edge: 'start',
checked: selectedPaths.includes(activity.item.path),
onClick: (e) => {
handleSelect(e, activity.item.path);
}
})
: React.createElement(Box, { sx: { minWidth: '30px' } })
),
React.createElement(ListItemText, {
primary: renderActivity(activity, {
formatMessage,
onPackageClick,
onItemClick
}),
secondary: renderActivityTimestamp(activity.actionTimestamp, locale)
}),
isItemActivity && React.createElement(DashletItemOptions, { path: activity.item.path })
)
);
})
),
total === 0 &&
React.createElement(
DashletEmptyMessage,
null,
React.createElement(FormattedMessage, {
id: 'activityDashlet.noEntriesFound',
defaultMessage: 'No activity was found.'
})
),
React.createElement(PackageDetailsDialog, {
open: openPackageDetailsDialog,
onClose: () => setState({ openPackageDetailsDialog: false }),
onClosed: () => setState({ selectedPackageId: null }),
packageId: selectedPackageId
})
);
}
export default MyRecentActivityDashlet;