@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
437 lines (435 loc) • 13.7 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 { 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
};
};