@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
227 lines (225 loc) • 8.45 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 { useActiveSiteId } from '../../hooks/useActiveSiteId';
import { FormattedMessage, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import useStyles from './styles';
import { useSelection } from '../../hooks/useSelection';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import translations from './translations';
import { withoutIndex } from '../../utils/path';
import { batchActions } from '../../state/actions/misc';
import { closeNewContentDialog, newContentCreationComplete } from '../../state/actions/dialogs';
import { fetchLegacyContentTypes } from '../../services/contentTypes';
import { showErrorDialog } from '../../state/reducers/dialogs/error';
import { useLogicResource } from '../../hooks/useLogicResource';
import { useSubject } from '../../hooks/useSubject';
import { debounceTime } from 'rxjs/operators';
import DialogBody from '../DialogBody/DialogBody';
import { Box, Checkbox, FormControlLabel } from '@mui/material';
import SingleItemSelector from '../SingleItemSelector';
import SearchBar from '../SearchBar/SearchBar';
import { SuspenseWithEmptyState } from '../Suspencified/Suspencified';
import DialogFooter from '../DialogFooter/DialogFooter';
import ContentTypesFilter from '../ContentTypeFilter';
import { ContentTypesGrid, ContentTypesLoader } from './NewContentDialog';
export function NewContentDialogContainer(props) {
const { item, onContentTypeSelected, compact = false, rootPath } = props;
const site = useActiveSiteId();
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const { classes } = useStyles();
const authoringBase = useSelection((state) => state.env.authoringBase);
const [isCompact, setIsCompact] = useState(compact);
const [openSelector, setOpenSelector] = useState(false);
const [selectedItem, setSelectedItem] = useState(item);
const [contentTypes, setContentTypes] = useState();
const [keyword, setKeyword] = useState('');
const [debounceKeyword, setDebounceKeyword] = useState('');
const [selectedFilter, setSelectedFilter] = useState('all');
const filters = [
{
label: formatMessage(translations.contentTypeAllLabel),
type: 'all'
},
{
label: formatMessage(translations.contentTypePageLabel),
type: 'page'
},
{
label: formatMessage(translations.contentTypeComponentLabel),
type: 'component'
}
];
const onSelectedContentType = useCallback(
(contentType) => {
const path = withoutIndex(selectedItem.path);
onContentTypeSelected?.({
authoringBase,
path,
isNewContent: true,
contentTypeId: contentType.form,
onSaveSuccess: batchActions([closeNewContentDialog(), newContentCreationComplete()])
});
},
[authoringBase, onContentTypeSelected, selectedItem?.path]
);
useEffect(() => {
if (selectedItem.path) {
// TODO: https://github.com/craftercms/craftercms/issues/4473
const path =
selectedItem.systemType === 'folder' && !selectedItem.path.endsWith('/')
? `${selectedItem.path}/`
: selectedItem.path;
fetchLegacyContentTypes(site, path).subscribe({
next(response) {
if (response.length === 1) {
dispatch(closeNewContentDialog());
onSelectedContentType(response[0]);
} else {
setContentTypes(response);
}
},
error(response) {
dispatch(showErrorDialog({ error: response }));
}
});
}
}, [dispatch, selectedItem, site, onSelectedContentType]);
const resource = useLogicResource(
useMemo(() => ({ contentTypes, selectedFilter, debounceKeyword }), [contentTypes, selectedFilter, debounceKeyword]),
{
shouldResolve: ({ contentTypes }) => Boolean(contentTypes),
shouldReject: () => null,
shouldRenew: (source, resource) => resource.complete,
resultSelector: ({ contentTypes, debounceKeyword, selectedFilter }) => {
return contentTypes.filter(
(contentType) =>
contentType.label.toLowerCase().includes(debounceKeyword.toLowerCase()) &&
(selectedFilter === 'all' || contentType.type === selectedFilter)
);
},
errorSelector: () => null
}
);
const onSearch$ = useSubject();
useEffect(() => {
onSearch$.pipe(debounceTime(400)).subscribe((keywords) => {
setDebounceKeyword(keywords);
});
});
const onSearch = (keyword) => {
onSearch$.next(keyword);
setKeyword(keyword);
};
return React.createElement(
React.Fragment,
null,
React.createElement(
DialogBody,
{ classes: { root: classes.dialogContent } },
React.createElement(
Box,
{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
React.createElement(
Box,
null,
React.createElement(SingleItemSelector, {
label: React.createElement(FormattedMessage, { id: 'words.item', defaultMessage: 'Item' }),
open: openSelector,
onClose: () => setOpenSelector(false),
onDropdownClick: () => setOpenSelector(!openSelector),
rootPath: rootPath,
selectedItem: selectedItem,
canSelectFolders: true,
onItemClicked: (item) => {
setOpenSelector(false);
setSelectedItem(item);
}
})
),
React.createElement(
Box,
{ className: classes.searchBox },
React.createElement(SearchBar, {
onChange: onSearch,
keyword: keyword,
autoFocus: true,
showActionButton: Boolean(keyword)
})
)
),
React.createElement(
SuspenseWithEmptyState,
{
resource: resource,
suspenseProps: {
fallback: React.createElement(ContentTypesLoader, { isCompact: isCompact })
},
withEmptyStateProps: {
emptyStateProps: {
classes: {
image: classes.emptyStateImg
},
title: React.createElement(FormattedMessage, {
id: 'newContentDialog.emptyStateMessage',
defaultMessage: 'No Content Types Found'
})
}
}
},
React.createElement(ContentTypesGrid, {
resource: resource,
isCompact: isCompact,
onTypeOpen: onSelectedContentType
})
)
),
React.createElement(
DialogFooter,
null,
React.createElement(FormControlLabel, {
className: classes.compact,
control: React.createElement(Checkbox, {
checked: isCompact,
onChange: () => setIsCompact(!isCompact),
color: 'primary'
}),
label: formatMessage(translations.compactInput)
}),
React.createElement(ContentTypesFilter, {
filters: filters,
selected: selectedFilter,
onFilterChange: setSelectedFilter
})
)
);
}