UNPKG

@craftercms/studio-ui

Version:

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

437 lines (435 loc) 13.7 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 { generateMultipleItemOptions, generateSingleItemOptions, itemActionDispatcher } from '../../utils/itemActions'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { useSelection } from '../../hooks/useSelection'; import { useActiveSiteId } from '../../hooks/useActiveSiteId'; import { useEnv } from '../../hooks/useEnv'; import { showEditDialog, showItemMegaMenu, showPreviewDialog, updatePreviewDialog } from '../../state/actions/dialogs'; import { getNumOfMenuOptionsForItem, getSystemTypeFromPath } from '../../utils/content'; import { search } from '../../services/search'; import { showErrorDialog } from '../../state/reducers/dialogs/error'; import { translations } from './translations'; import { contentEvent, deleteContentEvent, deleteContentEvents } from '../../state/actions/system'; import { getHostToHostBus } from '../../utils/subjects'; import { filter } from 'rxjs/operators'; import { fetchContentXML } from '../../services/content'; import { getPreviewURLFromPath } from '../../utils/path'; import useFetchSandboxItems from '../../hooks/useFetchSandboxItems'; export const drawerWidth = 300; export const SORT_AUTO = '-AUTO-'; export const initialSearchParameters = { query: '', keywords: '', offset: 0, limit: 21, sortBy: SORT_AUTO, sortOrder: 'desc', filters: {} }; export const actionsToBeShown = [ 'edit', 'delete', 'publish', 'rejectPublish', 'duplicate', 'duplicateAsset', 'dependencies', 'history' ]; export const setCheckedParameterFromURL = (queryParams) => { if (queryParams['filters']) { let checked = {}; let parseQP = JSON.parse(queryParams['filters']); Object.keys(parseQP).forEach((facet) => { if (Array.isArray(parseQP[facet])) { checked[facet] = {}; parseQP[facet].forEach((name) => { checked[facet][name] = true; }); } else { checked[facet] = parseQP[facet]; } }); return checked; } else { return {}; } }; export const serializeSearchFilters = (filters) => { const serializedFilters = {}; if (filters) { Object.entries(filters).forEach(([key, filter]) => { if (Array.isArray(filter)) { serializedFilters[key] = filter.reduce((checked, key) => ({ ...checked, [key]: true }), {}); } else if (filter.date) { serializedFilters[key] = `${filter.min ?? ''}TODATE${filter.max ?? ''}ID${filter.id}`; } else { serializedFilters[key] = `${filter.min ?? ''}TO${filter.max ?? ''}`; } }); } return serializedFilters; }; export const deserializeSearchFilters = (filters) => { const deserializedFilters = {}; if (filters) { Object.keys(filters).forEach((key) => { if (filters[key].includes('TODATE')) { let id = filters[key].split('ID'); let range = id[0].split('TODATE'); deserializedFilters[key] = { date: true, id: id[1], min: range[0] !== 'null' ? range[0] : null, max: range[1] !== 'null' ? range[1] : null }; } else if (filters[key].includes('TO')) { let range = filters[key].split('TO'); deserializedFilters[key] = { min: range[0] !== null && range[0] !== '' ? range[0] : null, max: range[1] !== null && range[1] !== '' ? range[1] : null }; } else if (typeof filters[key] === 'object' && !Array.isArray(filters[key])) { deserializedFilters[key] = Object.keys(filters[key]); } else { deserializedFilters[key] = filters[key]; } }); } return deserializedFilters; }; /** * Encapsulates logic to pick sortBy depending on whether there's a keyword. */ export function prepareSearchParams(searchParameters) { if (!searchParameters.sortBy || searchParameters.sortBy === SORT_AUTO) { return { ...searchParameters, sortBy: searchParameters.keywords ? '_score' : 'internalName', sortOrder: searchParameters.keywords ? 'desc' : 'asc' }; } return searchParameters; } export const useSearchState = ({ searchParameters, onSelect }) => { const { formatMessage } = useIntl(); const dispatch = useDispatch(); const clipboard = useSelection((state) => state.content.clipboard); const site = useActiveSiteId(); const { authoringBase, guestBase } = useEnv(); const [selected, setSelected] = useState([]); const [searchResults, setSearchResults] = useState(null); const [selectedPath, setSelectedPath] = useState(searchParameters.path ?? ''); useFetchSandboxItems(selected); const { itemsBeingFetchedByPath, itemsByPath } = useSelection((state) => state.content); const isFetching = selected.some((path) => itemsBeingFetchedByPath[path]); const [drawerOpen, setDrawerOpen] = useState(window.innerWidth > 960); const [currentView, setCurrentView] = useState('grid'); const [error, setError] = useState(null); const [isFetchingResults, setIsFetchingResults] = useState(false); const selectionOptions = useMemo(() => { if (selected.length === 0) { return null; } else if (selected.length) { if (selected.length === 1) { const path = selected[0]; const item = itemsByPath[path]; if (item) { return generateSingleItemOptions(item, formatMessage, { includeOnly: actionsToBeShown }).flat(); } } else { let items = []; selected.forEach((itemPath) => { const item = itemsByPath[itemPath]; if (item) { items.push(item); } }); if (items.length && !isFetching) { return generateMultipleItemOptions(items, formatMessage, { includeOnly: actionsToBeShown }); } } } }, [formatMessage, isFetching, itemsByPath, selected]); const areAllSelected = useMemo(() => { if (!searchResults || searchResults.items.length === 0) return false; return !searchResults.items.some((item) => !selected.includes(item.path)); }, [searchResults, selected]); const onActionClicked = (option, event) => { if (selected.length > 1) { const detailedItems = []; selected.forEach((path) => { itemsByPath?.[path] && detailedItems.push(itemsByPath[path]); }); itemActionDispatcher({ site, item: detailedItems, option, authoringBase, dispatch, formatMessage, clipboard, event }); } else { const path = selected[0]; const item = itemsByPath?.[path]; itemActionDispatcher({ site, item, option, authoringBase, dispatch, formatMessage, clipboard, event }); } }; const onHeaderButtonClick = (event, item) => { const path = item.path; dispatch( showItemMegaMenu({ path, anchorReference: 'anchorPosition', anchorPosition: { top: event.clientY, left: event.clientX }, numOfLoaderItems: getNumOfMenuOptionsForItem({ path: item.path, systemType: getSystemTypeFromPath(item.path) }) }) ); }; const refreshSearch = useCallback(() => { setError(null); setIsFetchingResults(true); search(site, prepareSearchParams(searchParameters)).subscribe({ next(result) { setSearchResults(result); setIsFetchingResults(false); }, error(error) { setIsFetchingResults(false); setSearchResults({ total: 0, items: [], facets: [] }); const { response } = error; if (response && response.response) { setError(response.response); } else { console.error(error); dispatch( showErrorDialog({ error: { message: formatMessage(translations.unknownError) } }) ); } } }); }, [dispatch, formatMessage, searchParameters, site]); const handleClearSelected = useCallback(() => { selected.forEach((path) => { onSelect?.(path, false); }); setSelected([]); }, [onSelect, selected]); const handleSelect = (path, isSelected) => { if (isSelected) { setSelected([...selected, path]); } else { let selectedItems = [...selected]; let index = selectedItems.indexOf(path); selectedItems.splice(index, 1); setSelected(selectedItems); } onSelect?.(path, isSelected); }; const handleSelectAll = (checked) => { if (checked) { let selectedItems = []; searchResults.items.forEach((item) => { if (selected.indexOf(item.path) === -1) { selectedItems.push(item.path); onSelect?.(item.path, true); } }); setSelected([...selected, ...selectedItems]); } else { let newSelectedItems = [...selected]; searchResults.items.forEach((item) => { let index = newSelectedItems.indexOf(item.path); if (index >= 0) { newSelectedItems.splice(index, 1); onSelect?.(item.path, false); } }); setSelected(newSelectedItems); } }; const onPreview = (item) => { let { type, name: title, path } = item; if (item.mimeType.includes('audio/')) { type = 'Audio'; } switch (type) { case 'Image': { dispatch( showPreviewDialog({ type: 'image', title, url: path }) ); break; } case 'Page': { dispatch( showPreviewDialog({ type: 'page', title, url: `${guestBase}${getPreviewURLFromPath(path)}?crafterCMSGuestDisabled=true` }) ); break; } case 'Component': case 'Taxonomy': { dispatch(showEditDialog({ site, path: item.path, authoringBase, readonly: true })); break; } case 'Video': dispatch( showPreviewDialog({ type: 'video', title, url: path }) ); break; case 'Audio': dispatch( showPreviewDialog({ type: 'audio', title, url: path, mimeType: item.mimeType }) ); break; default: { let mode = 'txt'; if (type === 'Template') { mode = 'ftl'; } else if (type === 'Groovy') { mode = 'groovy'; } else if (type === 'JavaScript') { mode = 'javascript'; } else if (type === 'CSS') { mode = 'css'; } dispatch( showPreviewDialog({ type: 'editor', title, url: path, path: path, mode }) ); fetchContentXML(site, path).subscribe((content) => { dispatch( updatePreviewDialog({ content }) ); }); break; } } }; const clearPath = () => { setSelectedPath(undefined); }; const onSelectedPathChanges = (path) => { setSelectedPath(path); }; const toggleDrawer = () => { setDrawerOpen(!drawerOpen); }; const handleChangeView = () => { if (currentView === 'grid') { setCurrentView('list'); } else { setCurrentView('grid'); } }; useEffect(() => { const eventsThatNeedReaction = [contentEvent.type, deleteContentEvent.type, deleteContentEvents.type]; const hostToHost$ = getHostToHostBus(); const subscription = hostToHost$.pipe(filter((e) => eventsThatNeedReaction.includes(e.type))).subscribe(() => { handleClearSelected(); refreshSearch(); }); return () => { subscription.unsubscribe(); }; }, [handleClearSelected, refreshSearch]); useEffect(() => { refreshSearch(); return () => setError(null); }, [refreshSearch]); return { error, selected, guestBase, drawerOpen, currentView, itemsByPath, selectedPath, searchResults, areAllSelected, selectionOptions, handleClearSelected, isFetching: isFetchingResults, clearPath, onPreview, toggleDrawer, handleSelect, handleSelectAll, onActionClicked, handleChangeView, onHeaderButtonClick, onSelectedPathChanges }; };