UNPKG

@craftercms/studio-ui

Version:

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

223 lines (221 loc) 9.39 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 { 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;