@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
223 lines (221 loc) • 9.39 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 { FormattedMessage, useIntl } from 'react-intl';
import { nnou, nou } from '../../utils/object';
import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';
import LoadingState from '../LoadingState/LoadingState';
import {
componentInstanceDragEnded,
componentInstanceDragStarted,
fetchComponentsByContentType,
setContentTypeFilter,
setPreviewEditMode
} from '../../state/actions/preview';
import { useDispatch } from 'react-redux';
import SearchBar from '../SearchBar/SearchBar';
import { getHostToGuestBus } from '../../utils/subjects';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import { useSelection } from '../../hooks/useSelection';
import { useDebouncedInput } from '../../hooks/useDebouncedInput';
import translations from './translations';
import useStyles from './styles';
import PreviewBrowseComponentsPanelUI from './PreviewBrowseComponentsPanelUI';
import { useActiveSiteId } from '../../hooks/useActiveSiteId';
import { ApiResponseErrorState } from '../ApiResponseErrorState';
import ListSubheader from '@mui/material/ListSubheader';
import Alert from '@mui/material/Alert';
export function PreviewBrowseComponentsPanel() {
const { classes } = useStyles();
const dispatch = useDispatch();
const siteId = useActiveSiteId();
const allowedTypesData = useSelection((state) => state.preview.guest?.allowedContentTypes);
const awaitingGuestCheckIn = nou(allowedTypesData);
const contentTypesUpdated = useSelection((state) => state.preview.guest?.contentTypesUpdated);
const componentsState = useSelection((state) => state.preview.components);
const [keyword, setKeyword] = useState(componentsState.query.keywords);
const contentTypesBranch = useSelection((state) => state.contentTypes);
const editMode = useSelection((state) => state.preview.editMode);
const contentTypes = contentTypesBranch.byId
? Object.values(contentTypesBranch.byId).filter(
(contentType) => contentType.type === 'component' && !contentType.id.includes('/level-descriptor')
)
: null;
const items = useMemo(() => {
let items = componentsState.page[componentsState.pageNumber]?.map((id) => componentsState.byId[id]) ?? [];
if (componentsState.contentTypeFilter !== 'compatible') {
items = items.filter((item) => item.craftercms.contentTypeId === componentsState.contentTypeFilter);
}
return items;
}, [componentsState]);
const contentTypeData = useMemo(() => {
const allowedTypes = [];
const otherTypes = [];
const result = { allowedTypes, otherTypes };
if (!contentTypes || !allowedTypesData) return result;
contentTypes.forEach((contentType) => {
if (allowedTypesData[contentType.id]?.shared) {
allowedTypes.push(contentType);
} else {
otherTypes.push(contentType);
}
const sorter = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
allowedTypes.sort(sorter);
otherTypes.sort(sorter);
});
return result;
}, [allowedTypesData, contentTypes]);
useEffect(() => {
// We want guest to check in, so we can retrieve the compatible types when fetching the items.
if (siteId && contentTypesBranch.isFetching === false && !awaitingGuestCheckIn) {
dispatch(fetchComponentsByContentType({ sortBy: 'internalName', sortOrder: 'asc' }));
}
}, [siteId, contentTypesBranch, dispatch, awaitingGuestCheckIn]);
const { formatMessage } = useIntl();
const hostToGuest$ = getHostToGuestBus();
const onDragStart = (item) => {
if (!editMode) {
dispatch(setPreviewEditMode({ editMode: true }));
}
hostToGuest$.next(
componentInstanceDragStarted({
instance: item,
contentType: contentTypesBranch.byId[item.craftercms.contentTypeId]
})
);
};
const onDragEnd = () => hostToGuest$.next({ type: componentInstanceDragEnded.type });
const onSearch = useCallback(
(keywords) =>
dispatch(fetchComponentsByContentType({ keywords, offset: 0, sortBy: 'internalName', sortOrder: 'asc' })),
[dispatch]
);
const onSearch$ = useDebouncedInput(onSearch, 600);
function onPageChanged(newPage) {
dispatch(fetchComponentsByContentType({ offset: newPage, sortBy: 'internalName', sortOrder: 'asc' }));
}
function onRowsPerPageChange(e) {
dispatch(
fetchComponentsByContentType({ offset: 0, limit: e.target.value, sortBy: 'internalName', sortOrder: 'asc' })
);
}
function handleSearchKeyword(keyword) {
setKeyword(keyword);
onSearch$.next(keyword);
}
function handleSelectChange(value) {
dispatch(setContentTypeFilter(value));
}
return React.createElement(
React.Fragment,
null,
React.createElement(
ErrorBoundary,
null,
contentTypesUpdated &&
React.createElement(
Alert,
{ severity: 'warning', variant: 'outlined', sx: { border: 0 } },
React.createElement(FormattedMessage, {
defaultMessage: 'Content type definitions have changed. Please refresh the preview application.'
})
),
React.createElement(
'div',
{ className: classes.search },
React.createElement(SearchBar, {
placeholder: formatMessage(translations.filter),
showActionButton: Boolean(keyword),
onChange: handleSearchKeyword,
keyword: keyword,
autoFocus: true
}),
contentTypes &&
React.createElement(
Select,
{
size: 'small',
value: componentsState.contentTypeFilter,
displayEmpty: true,
className: classes.select,
onChange: (event) => handleSelectChange(event.target.value)
},
React.createElement(
MenuItem,
{ value: 'compatible' },
React.createElement(FormattedMessage, { defaultMessage: 'Compatible Types' })
),
React.createElement(
ListSubheader,
null,
React.createElement(FormattedMessage, { defaultMessage: 'Compatible' })
),
contentTypeData.allowedTypes.length === 0
? React.createElement(
MenuItem,
{ disabled: true },
React.createElement(FormattedMessage, { defaultMessage: 'No compatible types were found.' })
)
: contentTypeData.allowedTypes.map((contentType, i) =>
React.createElement(MenuItem, { value: contentType.id, key: i }, contentType.name)
),
React.createElement(
ListSubheader,
null,
React.createElement(FormattedMessage, { defaultMessage: 'Other' })
),
contentTypeData.otherTypes.map((contentType, i) =>
React.createElement(MenuItem, { value: contentType.id, key: i }, contentType.name)
)
)
),
componentsState.error
? React.createElement(ApiResponseErrorState, { error: componentsState.error })
: componentsState.isFetching
? React.createElement(LoadingState, { title: formatMessage(translations.loading) })
: ((nnou(componentsState.pageNumber) && nnou(componentsState.page[componentsState.pageNumber])) ||
!componentsState.contentTypeFilter) &&
React.createElement(PreviewBrowseComponentsPanelUI, {
awaitingGuestCheckIn: awaitingGuestCheckIn,
items: items,
count: componentsState.count,
pageNumber: componentsState.pageNumber,
limit: componentsState.query.limit,
onPageChanged: onPageChanged,
onRowsPerPageChange: onRowsPerPageChange,
onDragStart: onDragStart,
onDragEnd: onDragEnd
})
)
);
}
export default PreviewBrowseComponentsPanel;