UNPKG

@craftercms/studio-ui

Version:

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

470 lines (468 loc) 17.5 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, { useEffect, useMemo } from 'react'; import { MoreVertRounded, RefreshRounded } from '@mui/icons-material'; import { PREVIEW_URL_PATH, UNDEFINED } from '../../utils/constants'; import useActiveSiteId from '../../hooks/useActiveSiteId'; import useEnv from '../../hooks/useEnv'; import useLocale from '../../hooks/useLocale'; import useSpreadState from '../../hooks/useSpreadState'; import { fetchActivity } from '../../services/dashboard'; import IconButton from '@mui/material/IconButton'; import { FormattedMessage, useIntl } from 'react-intl'; import MuiTimeline from '@mui/lab/Timeline'; import TimelineItem from '@mui/lab/TimelineItem'; import TimelineSeparator from '@mui/lab/TimelineSeparator'; import TimelineDot from '@mui/lab/TimelineDot'; import TimelineContent from '@mui/lab/TimelineContent'; import TimelineConnector from '@mui/lab/TimelineConnector'; import Typography from '@mui/material/Typography'; import DropDownMenu from '../DropDownMenuButton/DropDownMenuButton'; import { activityNameLookup, renderActivity, renderActivityTimestamp, useSelectionLookupState } from './utils'; import { AuthorFilter } from './AuthorFilter'; import Skeleton from '@mui/material/Skeleton'; import { styled } from '@mui/material/styles'; import { RangePickerModal } from './RangePickerModal'; import Tooltip from '@mui/material/Tooltip'; import DashletCard from '../DashletCard/DashletCard'; import { asLocalizedDateTime } from '../../utils/datetime'; import { DashletAvatar, DashletEmptyMessage, PersonAvatar, PersonFullName } from '../DashletCard/dashletCommons'; import { getSystemLink } from '../../utils/system'; import { useDispatch } from 'react-redux'; import { changeCurrentUrl } from '../../state/actions/preview'; import { useWidgetDialogContext } from '../WidgetDialog'; import PackageDetailsDialog from '../PackageDetailsDialog/PackageDetailsDialog'; const Timeline = styled(MuiTimeline)({ margin: 0, padding: 0 }); const CustomTimelineItem = styled(TimelineItem)({ minHeight: 0, '&.MuiTimelineItem-missingOppositeContent::before': { display: 'none', content: 'none' } }); const TimelineDotWithAvatar = styled(TimelineDot)({ padding: 0, border: 'none', alignSelf: 'auto' }); const TimelineDotWithoutAvatar = styled(TimelineDot)({ alignSelf: 'auto' }); const SizedTimelineSeparator = styled(TimelineSeparator)({ width: 40, minWidth: 40, textAlign: 'center', alignItems: 'center', alignContent: 'center', justifyContent: 'center' }); const emptyTimelineContentSx = { pt: 0, pb: 0 }; const getSkeletonTimelineItems = ({ items = 1 }) => new Array(items) .fill(null) .map((nothing, index) => React.createElement( CustomTimelineItem, { key: index }, React.createElement( SizedTimelineSeparator, null, React.createElement(TimelineConnector, null), React.createElement(TimelineDotWithAvatar, null, React.createElement(DashletAvatar, null)), React.createElement(TimelineConnector, null) ), React.createElement( TimelineContent, { sx: { py: '12px', px: 2 } }, React.createElement(Typography, { variant: 'h6' }, React.createElement(Skeleton, { variant: 'text' })), React.createElement(Typography, { variant: 'body1' }, React.createElement(Skeleton, { variant: 'text' })), React.createElement(Typography, { variant: 'body1' }, React.createElement(Skeleton, { variant: 'text' })) ) ) ); export function ActivityDashlet(props) { const { borderLeftColor = 'success.main' } = props; const locale = useLocale(); const site = useActiveSiteId(); const { authoringBase } = useEnv(); const { formatMessage } = useIntl(); const dispatch = useDispatch(); const widgetDialogContext = useWidgetDialogContext(); const [ { feed, feedType, usernames, dateFrom, dateTo, limit, offset, total, openRangePicker, loadingChunk, loadingFeed, selectedPackageId, openPackageDetailsDialog }, setState ] = useSpreadState({ loadingChunk: false, loadingFeed: false, openRangePicker: false, openPackageDetailsDialog: false, selectedPackageId: null, feed: null, usernames: null, feedType: 'timeline', dateFrom: null, dateTo: null, limit: 10, offset: 0, total: null }); const [selectedActivities, setSelectedActivities, activities] = useSelectionLookupState({ ALL: true }); // region activityFilterOptions = ... const activityFilterOptions = useMemo( () => Object.keys(activityNameLookup).map((key) => ({ id: key, primaryText: activityNameLookup[key], selected: selectedActivities[key] })), [selectedActivities] ); // endregion // region feedTypeOptions const feedTypeOptions = useMemo( () => [ { id: 'timeline', primaryText: React.createElement(FormattedMessage, { id: 'words.timeline', defaultMessage: 'Timeline' }), selected: feedType === 'timeline', secondaryText: React.createElement(FormattedMessage, { id: 'activityDashlet.timelineOptionTip', defaultMessage: 'All activity, most recent first' }) }, { id: 'range', primaryText: React.createElement(FormattedMessage, { id: 'words.range', defaultMessage: 'Range' }), selected: feedType === 'range', secondaryText: React.createElement(FormattedMessage, { id: 'activityDashlet.rangeOptionTip', defaultMessage: 'Specify a range of dates' }) } ], [feedType] ); // endregion // region onRefresh const onRefresh = useMemo( () => () => { setState({ feed: null, total: null, offset: 0, loadingFeed: true }); fetchActivity(site, { actions: activities.filter((key) => key !== 'ALL'), usernames, dateTo, dateFrom, limit }).subscribe((feed) => { setState({ feed, total: feed.total, loadingFeed: false }); }); }, [site, activities, dateFrom, dateTo, limit, setState, usernames] ); // endregion const loadNextPage = () => { let newOffset = offset + limit; setState({ loadingChunk: true }); fetchActivity(site, { actions: activities.filter((key) => key !== 'ALL'), usernames, dateTo, dateFrom, limit, offset: newOffset }).subscribe((nextFeedChunk) => { setState({ feed: feed.concat(nextFeedChunk), total: nextFeedChunk.total, offset: newOffset, loadingChunk: false }); }); }; const onItemClick = (previewUrl, e) => { const pathname = window.location.pathname; if (pathname.includes(PREVIEW_URL_PATH)) { dispatch(changeCurrentUrl(previewUrl)); widgetDialogContext === null || widgetDialogContext === void 0 ? void 0 : widgetDialogContext.onClose(e, null); } else { window.location.href = getSystemLink({ page: previewUrl, systemLinkId: 'preview', site, authoringBase }); } }; const onPackageClick = (pkg) => { setState({ openPackageDetailsDialog: true, selectedPackageId: pkg.id }); }; const hasMoreItemsToLoad = total > 0 && limit + offset < total; useEffect(() => { onRefresh(); }, [onRefresh, setState]); return React.createElement( DashletCard, Object.assign({}, props, { borderLeftColor: borderLeftColor, title: React.createElement(FormattedMessage, { id: 'words.activity', defaultMessage: 'Activity' }), headerAction: React.createElement(IconButton, { onClick: onRefresh }, React.createElement(RefreshRounded, null)), actionsBar: React.createElement( React.Fragment, null, React.createElement( DropDownMenu, { size: 'small', variant: 'text', onMenuItemClick: (e, id) => setSelectedActivities(id), options: activityFilterOptions, closeOnSelection: false, menuProps: { sx: { minWidth: 180 } }, listItemProps: { dense: true } }, selectedActivities.ALL ? React.createElement(FormattedMessage, { id: 'activityDashlet.showActivityByEveryone', defaultMessage: 'All activities' }) : React.createElement(FormattedMessage, { id: 'activityDashlet.showSelectedActivities', defaultMessage: 'Selected activities ({count})', values: { count: activities.length } }) ), React.createElement(AuthorFilter, { onChange: (users) => setState({ usernames: users.map(({ username }) => username) }) }), React.createElement( DropDownMenu, { size: 'small', variant: 'text', onMenuItemClick: (e, feedType) => { const state = { feedType, dateTo, dateFrom, openRangePicker: feedType === 'range' }; if (feedType === 'timeline') { state.dateTo = null; state.dateFrom = null; } setState(state); }, options: feedTypeOptions, menuProps: { sx: { minWidth: 200 } }, listItemProps: { dense: true }, onClick: () => { if (feedType === 'range') { setState({ openRangePicker: true }); return false; } } }, React.createElement(FormattedMessage, { id: 'words.timeline', defaultMessage: 'Timeline' }) ) ) }), loadingFeed && React.createElement( Timeline, { position: 'right' }, React.createElement( CustomTimelineItem, null, React.createElement(SizedTimelineSeparator, null, React.createElement(TimelineDotWithoutAvatar, null)), React.createElement(TimelineContent, { sx: emptyTimelineContentSx }) ), getSkeletonTimelineItems({ items: 3 }), React.createElement( CustomTimelineItem, null, React.createElement( SizedTimelineSeparator, null, hasMoreItemsToLoad ? React.createElement( TimelineDotWithAvatar, { sx: { bgcolor: 'unset' } }, React.createElement( Tooltip, { title: React.createElement(FormattedMessage, { id: 'activityDashlet.loadMore', defaultMessage: 'Load {limit} more', values: { limit } }) }, React.createElement( IconButton, { color: 'primary', size: 'small', onClick: loadNextPage }, React.createElement(MoreVertRounded, null) ) ) ) : React.createElement(TimelineDotWithoutAvatar, null) ), React.createElement(TimelineContent, { sx: emptyTimelineContentSx }) ) ), Boolean(feed === null || feed === void 0 ? void 0 : feed.length) && React.createElement( Timeline, { position: 'right' }, React.createElement( CustomTimelineItem, null, React.createElement(SizedTimelineSeparator, null, React.createElement(TimelineDotWithoutAvatar, null)), React.createElement(TimelineContent, { sx: emptyTimelineContentSx }) ), feed.map((activity) => React.createElement( CustomTimelineItem, { key: activity.id }, React.createElement( SizedTimelineSeparator, null, React.createElement(TimelineConnector, null), React.createElement( TimelineDotWithAvatar, null, React.createElement(PersonAvatar, { person: activity.person }) ), React.createElement(TimelineConnector, null) ), React.createElement( TimelineContent, { sx: { py: '12px', px: 2 } }, React.createElement(PersonFullName, { person: activity.person }), React.createElement( Typography, null, renderActivity(activity, { formatMessage, onPackageClick, onItemClick }) ), React.createElement( Typography, { variant: 'caption', title: asLocalizedDateTime(activity.actionTimestamp, locale.localeCode, locale.dateTimeFormatOptions) }, renderActivityTimestamp(activity.actionTimestamp, locale) ) ) ) ), loadingChunk ? getSkeletonTimelineItems({ items: 3 }) : React.createElement( CustomTimelineItem, null, React.createElement( SizedTimelineSeparator, null, hasMoreItemsToLoad ? React.createElement( TimelineDotWithAvatar, { sx: { bgcolor: 'unset' } }, React.createElement( Tooltip, { title: React.createElement(FormattedMessage, { id: 'activityDashlet.loadMore', defaultMessage: 'Load {limit} more', values: { limit } }) }, React.createElement( IconButton, { color: 'primary', size: 'small', onClick: loadNextPage }, React.createElement(MoreVertRounded, null) ) ) ) : React.createElement(TimelineDotWithoutAvatar, null) ), React.createElement( TimelineContent, { sx: hasMoreItemsToLoad ? { py: '12px', px: 2, display: 'flex', alignItems: 'center' } : emptyTimelineContentSx }, hasMoreItemsToLoad && React.createElement( Typography, { variant: 'body2', color: 'text.secondary' }, React.createElement(FormattedMessage, { id: 'activityDashlet.hasMoreItemsToLoadMessage', defaultMessage: '{count} more {count, plural, one {activity} other {activities}} available', values: { count: total - (limit + offset) } }) ) ) ) ), total === 0 && React.createElement( DashletEmptyMessage, null, React.createElement(FormattedMessage, { id: 'activityDashlet.noEntriesFound', defaultMessage: 'No activity was found.' }) ), React.createElement(RangePickerModal, { open: openRangePicker, onClose: () => setState({ openRangePicker: false }), onAccept: (dateFrom, dateTo) => setState({ dateFrom: dateFrom.toISOString(), dateTo: dateTo.toISOString(), openRangePicker: false }), onSwitchToTimelineClick: feedType === 'range' ? () => { setState({ feedType: 'timeline', dateTo: null, dateFrom: null, openRangePicker: false }); } : UNDEFINED }), React.createElement(PackageDetailsDialog, { open: openPackageDetailsDialog, onClose: () => setState({ openPackageDetailsDialog: false }), onClosed: () => setState({ selectedPackageId: null }), packageId: selectedPackageId }) ); } export default ActivityDashlet;