{
const makeSearch = useCallback(() => {
prevSelectedLanguage.current = selectedLanguage;
- search(0);
+ search();
}, [search, selectedLanguage]);
const isApplyButtonEnabled =
!!selectedContentTypes.length || !!selectedSection || !!selectedSubtree || prevSelectedLanguage.current !== selectedLanguage;
@@ -115,12 +118,9 @@ const Filters = ({ search }) => {
);
};
- const filtersLabel = Translator.trans(/*@Desc("Filters")*/ 'filters.title', {}, 'ibexa_universal_discovery_widget');
const languageLabel = Translator.trans(/*@Desc("Language")*/ 'filters.language', {}, 'ibexa_universal_discovery_widget');
const sectionLabel = Translator.trans(/*@Desc("Section")*/ 'filters.section', {}, 'ibexa_universal_discovery_widget');
const subtreeLabel = Translator.trans(/*@Desc("Subtree")*/ 'filters.subtree', {}, 'ibexa_universal_discovery_widget');
- const clearLabel = Translator.trans(/*@Desc("Clear")*/ 'filters.clear', {}, 'ibexa_universal_discovery_widget');
- const applyLabel = Translator.trans(/*@Desc("Apply")*/ 'filters.apply', {}, 'ibexa_universal_discovery_widget');
const languageOptions = Object.values(adminUiConfig.languages.mappings)
.filter((language) => language.enabled)
.map((language) => ({
@@ -150,25 +150,8 @@ const Filters = ({ search }) => {
return (
<>
{isNestedUdwOpened && ReactDOM.createPortal(
, nestedUdwContainer.current)}
-
-
-
{filtersLabel}
-
-
-
-
-
-
-
{languageLabel}
+
+
{
options={languageOptions}
extraClasses="c-udw-dropdown"
/>
-
+
-
-
{sectionLabel}
+
{
options={sectionOptions}
extraClasses="c-udw-dropdown"
/>
-
-
-
{subtreeLabel}
+
+
{renderSubtreeBreadcrumbs()}
{renderSelectContentButton()}
-
-
+
+
>
);
};
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.panel.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.panel.js
new file mode 100644
index 0000000000..d0d85f5adc
--- /dev/null
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.panel.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper';
+
+const FiltersPanel = ({ children, isApplyButtonEnabled, makeSearch, clearFilters }) => {
+ const Translator = getTranslator();
+ const filtersLabel = Translator.trans(/*@Desc("Filters")*/ 'filters.title', {}, 'ibexa_universal_discovery_widget');
+ const clearLabel = Translator.trans(/*@Desc("Clear")*/ 'filters.clear', {}, 'ibexa_universal_discovery_widget');
+ const applyLabel = Translator.trans(/*@Desc("Apply")*/ 'filters.apply', {}, 'ibexa_universal_discovery_widget');
+
+ return (
+
+
+
{filtersLabel}
+
+
+
+
+
+ {children}
+
+ );
+};
+
+FiltersPanel.propTypes = {
+ children: PropTypes.node,
+ isApplyButtonEnabled: PropTypes.bool.isRequired,
+ makeSearch: PropTypes.func.isRequired,
+ clearFilters: PropTypes.func.isRequired,
+};
+
+FiltersPanel.defaultProps = {
+ children: null,
+};
+
+export default FiltersPanel;
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.row.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.row.js
new file mode 100644
index 0000000000..7e50b12dd3
--- /dev/null
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.row.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { createCssClassNames } from '../../../common/helpers/css.class.names';
+
+const FiltersRow = ({ children, title, extraClasses }) => {
+ const className = createCssClassNames({
+ 'c-filters-row': true,
+ [extraClasses]: true,
+ });
+
+ return (
+
+ );
+};
+
+FiltersRow.propTypes = {
+ children: PropTypes.node.isRequired,
+ title: PropTypes.string.isRequired,
+ extraClasses: PropTypes.string,
+};
+
+FiltersRow.defaultProps = {
+ extraClasses: '',
+};
+
+export default FiltersRow;
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/search/search.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/search/search.js
index 1b316dab8a..2e352cd08a 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/components/search/search.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/search/search.js
@@ -11,6 +11,7 @@ import Icon from '../../../common/icon/icon';
import Spinner from '../../../common/spinner/spinner';
import ContentTable from '../content-table/content.table';
import Filters from '../filters/filters';
+import ContentMetaPreview from '../../content.meta.preview.module';
import SearchTags from './search.tags';
import { useSearchByQueryFetch } from '../../hooks/useSearchByQueryFetch';
import { ActiveTabContext, AllowedContentTypesContext, MarkedLocationIdContext, SearchTextContext } from '../../universal.discovery.module';
@@ -193,15 +194,18 @@ const Search = ({ itemsPerPage }) => {
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.item.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.item.js
new file mode 100644
index 0000000000..f369cde86d
--- /dev/null
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.item.js
@@ -0,0 +1,84 @@
+import React, { useContext, useMemo, useRef } from 'react';
+
+import {
+ parse as parseTooltip,
+ hideAll as hideAllTooltips,
+} from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/tooltips.helper';
+import { getAdminUiConfig, getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper';
+
+import Icon from '../../../common/icon/icon';
+import Thumbnail from '../../../common/thumbnail/thumbnail';
+
+import { SelectedItemsContext } from '../../universal.discovery.module';
+
+import { REMOVE_SELECTED_ITEMS } from '../../hooks/useSelectedItemsReducer';
+
+const SelectedItemsPanelItem = ({ item, thumbnailData, name, description }) => {
+ const adminUiConfig = getAdminUiConfig();
+ const Translator = getTranslator();
+ const refSelectedLocationsItem = useRef(null);
+ const { dispatchSelectedItemsAction } = useContext(SelectedItemsContext);
+ const removeItemLabel = Translator.trans(
+ /*@Desc("Clear selection")*/ 'selected_items_panel.item.remove_item',
+ {},
+ 'ibexa_universal_discovery_widget',
+ );
+ const removeFromSelection = () => {
+ hideAllTooltips(refSelectedLocationsItem.current);
+ dispatchSelectedItemsAction({ type: REMOVE_SELECTED_ITEMS, ids: [{ id: item.id, type: item.type }] });
+ };
+ const sortedActions = useMemo(() => {
+ const { universalSelectItemActions } = adminUiConfig.universalDiscoveryWidget;
+ const actions = universalSelectItemActions ? [...universalSelectItemActions] : [];
+
+ return actions.sort((actionA, actionB) => {
+ return actionB.priority - actionA.priority;
+ });
+ }, []);
+
+ return (
+
{
+ refSelectedLocationsItem.current = node;
+ parseTooltip(node);
+ }}
+ >
+
+
+
+
+ {name}
+ {description}
+
+
+ {sortedActions.map((action) => {
+ const Component = action.component;
+
+ return ;
+ })}
+
+
+
+ );
+};
+
+SelectedItemsPanelItem.propTypes = {
+ item: PropTypes.object.isRequired,
+ thumbnailData: PropTypes.shape({
+ mimeType: PropTypes.string.isRequired,
+ resource: PropTypes.string.isRequired,
+ }).isRequired,
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+};
+
+export default SelectedItemsPanelItem;
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.js
new file mode 100644
index 0000000000..50199d90dd
--- /dev/null
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.js
@@ -0,0 +1,154 @@
+import React, { useContext, useState, useEffect, useRef, useMemo } from 'react';
+
+import {
+ parse as parseTooltip,
+ hideAll as hideAllTooltips,
+} from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/tooltips.helper';
+import {
+ getBootstrap,
+ getAdminUiConfig,
+ getTranslator,
+} from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper';
+
+import Icon from '../../../common/icon/icon';
+import { createCssClassNames } from '../../../common/helpers/css.class.names';
+
+import { AllowConfirmationContext, SelectedItemsContext } from '../../universal.discovery.module';
+
+import { CLEAR_SELECTED_ITEMS } from '../../hooks/useSelectedItemsReducer';
+
+const SelectedItemsPanel = () => {
+ const Translator = getTranslator();
+ const adminUiConfig = getAdminUiConfig();
+ const itemsComponentsMap = useMemo(() => {
+ const { universalSelectItemsComponentsConfigs } = adminUiConfig.universalDiscoveryWidget;
+ const configsArray = universalSelectItemsComponentsConfigs ? [...universalSelectItemsComponentsConfigs] : [];
+
+ return configsArray.reduce((configsMap, config) => {
+ configsMap[config.itemType] = config;
+
+ return configsMap;
+ }, {});
+ }, [adminUiConfig]);
+ const refSelectedLocations = useRef(null);
+ const { selectedItems, dispatchSelectedItemsAction } = useContext(SelectedItemsContext);
+ const allowConfirmation = useContext(AllowConfirmationContext);
+ const [isExpanded, setIsExpanded] = useState(false);
+ const className = createCssClassNames({
+ 'c-selected-items-panel': true,
+ 'c-selected-items-panel--expanded': isExpanded,
+ });
+ const expandLabel = Translator.trans(
+ /*@Desc("Expand sidebar")*/ 'selected_items.expand.sidebar',
+ {},
+ 'ibexa_universal_discovery_widget',
+ );
+ const collapseLabel = Translator.trans(
+ /*@Desc("Collapse sidebar")*/ 'selected_items.collapse.sidebar',
+ {},
+ 'ibexa_universal_discovery_widget',
+ );
+ const togglerLabel = isExpanded ? collapseLabel : expandLabel;
+ const clearSelection = () => {
+ hideAllTooltips(refSelectedLocations.current);
+ dispatchSelectedItemsAction({ type: CLEAR_SELECTED_ITEMS });
+ };
+ const toggleExpanded = () => {
+ setIsExpanded(!isExpanded);
+ };
+ const renderSelectionCounter = () => {
+ const selectedLabel = Translator.transChoice(
+ /*@Desc("{1}%count% selected item|[2,Inf]%count% selected items")*/ 'selected_items.selection_info',
+ selectedItems.length,
+ { count: selectedItems.length },
+ 'ibexa_universal_discovery_widget',
+ );
+
+ return
{selectedLabel}
;
+ };
+ const renderToggleBtn = () => {
+ return (
+
+ );
+ };
+ const renderActionBtns = () => {
+ const removeLabel = Translator.transChoice(
+ /*@Desc("{1}Deselect|[2,Inf]Deselect all")*/ 'selected_items.deselect_all',
+ selectedItems.length,
+ {},
+ 'ibexa_universal_discovery_widget',
+ );
+
+ return (
+
+
+
+ );
+ };
+ const renderLocationsList = () => {
+ if (!isExpanded) {
+ return null;
+ }
+
+ return (
+
+ {renderActionBtns()}
+
+ {selectedItems.map((selectedItem) => {
+ const ItemComponent = itemsComponentsMap[selectedItem.type].component;
+
+ if (!ItemComponent) {
+ throw new Error(`SelectedItemsPanel: component for ${selectedItem.type} not provided in configuration.`);
+ }
+
+ return ;
+ })}
+
+
+ );
+ };
+
+ useEffect(() => {
+ if (!allowConfirmation) {
+ return;
+ }
+
+ parseTooltip(refSelectedLocations.current);
+ hideAllTooltips();
+
+ const bootstrap = getBootstrap();
+ const toggleBtnTooltip = bootstrap.Tooltip.getOrCreateInstance('.c-selected-items-panel__toggle-button');
+
+ toggleBtnTooltip.setContent({ '.tooltip-inner': togglerLabel });
+ }, [isExpanded]);
+
+ if (!allowConfirmation) {
+ return null;
+ }
+
+ return (
+
+
+ {renderToggleBtn()}
+ {renderSelectionCounter()}
+
+ {renderLocationsList()}
+
+ );
+};
+
+export default SelectedItemsPanel;
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-locations/selected.locations.item.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-locations/selected.locations.item.js
index 010f4b082d..57a1821f45 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-locations/selected.locations.item.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-locations/selected.locations.item.js
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useMemo, useRef } from 'react';
+import React, { useContext, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import {
@@ -38,12 +38,14 @@ const SelectedLocationsItem = ({ location, permissions }) => {
const version = location.ContentInfo.Content.CurrentVersion.Version;
const thumbnailData = version ? version.Thumbnail : {};
- useEffect(() => {
- parseTooltip(refSelectedLocationsItem.current);
- }, []);
-
return (
-
+
{
+ refSelectedLocationsItem.current = node;
+ parseTooltip(node);
+ }}
+ >
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-locations/selected.locations.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-locations/selected.locations.js
index 25ecc75839..7f809b2365 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-locations/selected.locations.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-locations/selected.locations.js
@@ -15,7 +15,6 @@ import { SelectedLocationsContext, AllowConfirmationContext } from '../../univer
const SelectedLocations = () => {
const Translator = getTranslator();
const refSelectedLocations = useRef(null);
- const refTogglerButton = useRef(null);
const [selectedLocations, dispatchSelectedLocationsAction] = useContext(SelectedLocationsContext);
const allowConfirmation = useContext(AllowConfirmationContext);
const [isExpanded, setIsExpanded] = useState(false);
@@ -52,18 +51,15 @@ const SelectedLocations = () => {
return
{selectedLabel}
;
};
const renderToggleButton = () => {
- const iconName = isExpanded ? 'caret-double-next' : 'caret-double-back';
-
return (
);
};
@@ -129,8 +125,8 @@ const SelectedLocations = () => {
return (
- {renderSelectionCounter()}
{renderToggleButton()}
+ {renderSelectionCounter()}
{renderLocationsList()}
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/sort-switcher/sort.switcher.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/sort-switcher/sort.switcher.js
index c398082678..f98cdc18ac 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/components/sort-switcher/sort.switcher.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/sort-switcher/sort.switcher.js
@@ -1,10 +1,12 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
+import { parse as parseTooltip } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/tooltips.helper';
+
import SimpleDropdown from '../../../common/simple-dropdown/simple.dropdown';
import { SortingContext, SortOrderContext, SORTING_OPTIONS } from '../../universal.discovery.module';
-const SortSwitcher = ({ isDisabled }) => {
+const SortSwitcher = ({ isDisabled, disabledConfig }) => {
const [sorting, setSorting] = useContext(SortingContext);
const [sortOrder, setSortOrder] = useContext(SortOrderContext);
const selectedOption = SORTING_OPTIONS.find((option) => option.sortClause === sorting && option.sortOrder === sortOrder);
@@ -12,9 +14,19 @@ const SortSwitcher = ({ isDisabled }) => {
setSorting(option.sortClause);
setSortOrder(option.sortOrder);
};
+ const disabledParams = {};
+
+ if (isDisabled && disabledConfig) {
+ disabledParams.title = disabledConfig.disabledInfoTooltipLabel;
+ }
return (
-
+
parseTooltip(node)}
+ className="c-sort-switcher"
+ data-tooltip-container-selector=".c-udw-tab"
+ {...disabledParams}
+ >
{
SortSwitcher.propTypes = {
isDisabled: PropTypes.bool,
+ disabledConfig: PropTypes.object,
};
SortSwitcher.defaultProps = {
isDisabled: false,
+ disabledConfig: null,
};
export const SortSwitcherMenuButton = {
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/tab/tab.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/tab/tab.js
index 2ac0160ae2..4ef0812cd4 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/components/tab/tab.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/tab/tab.js
@@ -8,15 +8,16 @@ import SelectedLocations from '../selected-locations/selected.locations';
import ContentCreateWidget from '../content-create-widget/content.create.widget';
import ContentMetaPreview from '../../content.meta.preview.module';
-import { SelectedLocationsContext, DropdownPortalRefContext } from '../../universal.discovery.module';
+import { SelectedLocationsContext, DropdownPortalRefContext, SelectedItemsContext } from '../../universal.discovery.module';
+import SelectedItemsPanel from '../selected-items/selected.items.panel';
-const Tab = ({ children, actionsDisabledMap }) => {
+const Tab = ({ children, actionsDisabledMap, isRightSidebarHidden }) => {
const topBarRef = useRef();
const bottomBarRef = useRef();
const [contentHeight, setContentHeight] = useState('100%');
const [selectedLocations] = useContext(SelectedLocationsContext);
+ const { selectedItems } = useContext(SelectedItemsContext);
const dropdownPortalRef = useContext(DropdownPortalRefContext);
- const selectedLocationsComponent = !!selectedLocations.length ? : null;
const contentStyles = {
height: contentHeight,
};
@@ -40,12 +41,15 @@ const Tab = ({ children, actionsDisabledMap }) => {
{children}
-
- {ContentMetaPreview && }
- {selectedLocationsComponent}
-
+ {!isRightSidebarHidden && (
+
+
+
+ )}
+ {!!selectedLocations.length &&
}
+ {!!selectedItems.length &&
}
@@ -56,6 +60,7 @@ const Tab = ({ children, actionsDisabledMap }) => {
Tab.propTypes = {
children: PropTypes.any.isRequired,
actionsDisabledMap: PropTypes.object,
+ isRightSidebarHidden: PropTypes.bool,
};
Tab.defaultProps = {
@@ -64,6 +69,7 @@ Tab.defaultProps = {
'sort-switcher': false,
'view-switcher': false,
},
+ isRightSidebarHidden: false,
};
export default Tab;
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/toggle-selection/toggle.item.selection.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/toggle-selection/toggle.item.selection.js
new file mode 100644
index 0000000000..e6a4a34b83
--- /dev/null
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/toggle-selection/toggle.item.selection.js
@@ -0,0 +1,42 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+
+import { createCssClassNames } from '../../../common/helpers/css.class.names';
+import { MultipleConfigContext, SelectedItemsContext } from '../../universal.discovery.module';
+
+const ToggleItemSelection = ({ item, isDisabled, isHidden }) => {
+ const { selectedItems } = useContext(SelectedItemsContext);
+ const [multiple, multipleItemsLimit] = useContext(MultipleConfigContext);
+ const isSelected = selectedItems.some((selectedItem) => selectedItem.type === item.type && selectedItem.id === item.id);
+ const isSelectionBlocked = multipleItemsLimit !== 0 && selectedItems.length >= multipleItemsLimit && !isSelected;
+ const className = createCssClassNames({
+ 'c-udw-toggle-selection ibexa-input': true,
+ 'ibexa-input--checkbox': multiple,
+ 'ibexa-input--radio': !multiple,
+ 'c-udw-toggle-selection--hidden': isHidden,
+ });
+ const inputType = multiple ? 'checkbox' : 'radio';
+
+ return (
+
+ );
+};
+
+ToggleItemSelection.propTypes = {
+ item: PropTypes.object.isRequired,
+ isHidden: PropTypes.bool,
+ isDisabled: PropTypes.bool,
+};
+
+ToggleItemSelection.defaultProps = {
+ isHidden: false,
+ isDisabled: false,
+};
+
+export default ToggleItemSelection;
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/top-menu/top.menu.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/top-menu/top.menu.js
index 4e6555c1cb..a04dc24624 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/components/top-menu/top.menu.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/top-menu/top.menu.js
@@ -36,8 +36,12 @@ const TopMenu = ({ actionsDisabledMap }) => {
{sortedActions.map((action) => {
const Component = action.component;
+ const disabledData = actionsDisabledMap[action.id];
+ const hasDisabledConfig = disabledData instanceof Object;
- return ;
+ return (
+
+ );
})}
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/top-menu/top.menu.search.input.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/top-menu/top.menu.search.input.js
index d66c4dbe7a..167a643fbe 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/components/top-menu/top.menu.search.input.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/top-menu/top.menu.search.input.js
@@ -4,14 +4,12 @@ import PropTypes from 'prop-types';
import { createCssClassNames } from '../../../common/helpers/css.class.names';
import Icon from '../../../common/icon/icon';
-import { ActiveTabContext, SearchTextContext } from '../../universal.discovery.module';
+import { SearchTextContext } from '../../universal.discovery.module';
const ENTER_CHAR_CODE = 13;
-const SEARCH_TAB_ID = 'search';
const TopMenuSearchInput = ({ isSearchOpened, setIsSearchOpened }) => {
- const [activeTab, setActiveTab] = useContext(ActiveTabContext);
- const [searchText, setSearchText] = useContext(SearchTextContext);
+ const [searchText, , makeSearch] = useContext(SearchTextContext);
const [inputValue, setInputValue] = useState(searchText);
const inputRef = useRef();
const className = createCssClassNames({
@@ -24,16 +22,9 @@ const TopMenuSearchInput = ({ isSearchOpened, setIsSearchOpened }) => {
'ibexa-btn--tertiary': !isSearchOpened,
});
const updateInputValue = ({ target: { value } }) => setInputValue(value);
- const search = (value) => {
- if (activeTab !== SEARCH_TAB_ID) {
- setActiveTab('search');
- }
-
- setSearchText(value);
- };
const handleSearchBtnClick = () => {
if (isSearchOpened) {
- search(inputValue);
+ makeSearch(inputValue);
setIsSearchOpened(false);
} else {
setIsSearchOpened(true);
@@ -41,7 +32,7 @@ const TopMenuSearchInput = ({ isSearchOpened, setIsSearchOpened }) => {
};
const handleKeyPressed = ({ charCode }) => {
if (charCode === ENTER_CHAR_CODE) {
- search(inputValue);
+ makeSearch(inputValue);
}
};
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/view-switcher/view.switcher.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/view-switcher/view.switcher.js
index 770e5df0fc..e4ef9a5178 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/components/view-switcher/view.switcher.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/view-switcher/view.switcher.js
@@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
import SimpleDropdown from '../../../common/simple-dropdown/simple.dropdown';
import { getTranslator } from '../../../../../../Resources/public/js/scripts/helpers/context.helper';
-import { CurrentViewContext, VIEWS } from '../../universal.discovery.module';
+import { CurrentViewContext, ViewContext } from '../../universal.discovery.module';
const ViewSwitcher = ({ isDisabled }) => {
const Translator = getTranslator();
const viewLabel = Translator.trans(/*@Desc("View")*/ 'view_switcher.view', {}, 'ibexa_universal_discovery_widget');
const [currentView, setCurrentView] = useContext(CurrentViewContext);
- const selectedOption = VIEWS.find((option) => option.value === currentView);
+ const { views } = useContext(ViewContext);
+ const selectedOption = views.find((option) => option.value === currentView);
const onOptionClick = ({ value }) => {
setCurrentView(value);
};
@@ -17,7 +18,7 @@ const ViewSwitcher = ({ isDisabled }) => {
return (
{
+ switch (action.type) {
+ case FETCH_START:
+ return {
+ ...state,
+ data: null,
+ isLoading: true,
+ };
+ case FETCH_END:
+ return { ...state, data: action.data, isLoading: false };
+ case CHANGE_PAGE: {
+ const isCurrentPageIndex = action.pageIndex === state.pageIndex;
+
+ if (isCurrentPageIndex) {
+ return state;
+ }
+
+ return {
+ ...state,
+ data: null,
+ pageIndex: action.pageIndex,
+ };
+ }
+ default:
+ throw new Error();
+ }
+};
+
+export const usePaginableFetch = ({ itemsPerPage, extraFetchParams }, fetchFunction) => {
+ const restInfo = useContext(RestInfoContext);
+ const [state, dispatch] = useReducer(fetchReducer, fetchInitialState);
+ const changePage = (pageIndex) => dispatch({ type: CHANGE_PAGE, pageIndex });
+
+ useEffect(() => {
+ dispatch({ type: FETCH_START });
+
+ const offset = state.pageIndex * itemsPerPage;
+ const { abortController } = fetchFunction({ ...restInfo, limit: itemsPerPage, offset, ...extraFetchParams }, (data) =>
+ dispatch({ type: FETCH_END, data }),
+ );
+
+ return () => {
+ if (abortController) {
+ abortController.abort();
+ }
+ };
+ }, [state.pageIndex, restInfo, itemsPerPage, extraFetchParams]);
+
+ return [state.data, state.isLoading, state.pageIndex, changePage];
+};
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/hooks/useSelectedItemsReducer.js b/src/bundle/ui-dev/src/modules/universal-discovery/hooks/useSelectedItemsReducer.js
new file mode 100644
index 0000000000..33ca0c9c79
--- /dev/null
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/hooks/useSelectedItemsReducer.js
@@ -0,0 +1,80 @@
+import { useReducer } from 'react';
+
+export const ADD_SELECTED_ITEMS = 'ADD_SELECTED_ITEMS';
+export const REMOVE_SELECTED_ITEMS = 'REMOVE_SELECTED_ITEMS';
+export const TOGGLE_SELECTED_ITEMS = 'TOGGLE_SELECTED_ITEMS';
+export const CLEAR_SELECTED_ITEMS = 'CLEAR_SELECTED_ITEMS';
+export const CHANGE_MULTIPLE_SETTING = 'CHANGE_MULTIPLE_SETTING';
+
+const checkIsItemSelected = (selectedItems, item) =>
+ selectedItems.some((selectedItem) => selectedItem.type === item.type && selectedItem.id === item.id);
+
+const filterOutSelectedItems = (selectedItems, items) => items.filter((item) => !checkIsItemSelected(selectedItems, item));
+
+const checkIsValidSelection = (items, isMultiple, multipleItemsLimit) =>
+ (!isMultiple && items.length > 1) || (isMultiple && multipleItemsLimit !== 0 && items.length > multipleItemsLimit);
+
+const selectedItemsReducer = (state, action) => {
+ const { items, isMultiple, multipleItemsLimit } = state;
+
+ switch (action.type) {
+ case ADD_SELECTED_ITEMS: {
+ const oldItemsWithoutNewItems = filterOutSelectedItems(action.items, items);
+ const newItems = [...oldItemsWithoutNewItems, ...action.items];
+
+ if (checkIsValidSelection(newItems, isMultiple, multipleItemsLimit)) {
+ throw new Error('useSelectedItemsReducer ADD_SELECTED_ITEMS: cannot select more than one item with single select.');
+ }
+
+ return {
+ ...state,
+ items: newItems,
+ };
+ }
+ case REMOVE_SELECTED_ITEMS:
+ return filterOutSelectedItems(action.itemsIdsWithTypes, items);
+ case TOGGLE_SELECTED_ITEMS: {
+ const oldItemsWithoutDeselectedItems = filterOutSelectedItems(action.items, items);
+ const newItemsWithoutDeselectedItems = filterOutSelectedItems(items, action.items);
+ const newItems = [...oldItemsWithoutDeselectedItems, ...newItemsWithoutDeselectedItems];
+
+ if (checkIsValidSelection(newItems, isMultiple, multipleItemsLimit)) {
+ throw new Error('useSelectedItemsReducer ADD_SELECTED_ITEMS: cannot select more than one item with single select.');
+ }
+
+ return {
+ ...state,
+ items: newItems,
+ };
+ }
+ case CLEAR_SELECTED_ITEMS:
+ return {
+ ...state,
+ items: [],
+ };
+ case CHANGE_MULTIPLE_SETTING:
+ if (!action.isMultiple && items.length > 1) {
+ throw new Error(
+ 'useSelectedItemsReducer CHANGE_MULTIPLE_SETTING: cannot set to single select when multiple items are selected.',
+ );
+ }
+
+ return {
+ ...state,
+ isMultiple: action.isMultiple,
+ };
+ default:
+ throw new Error();
+ }
+};
+
+export const useSelectedItemsReducer = ({ items = [], isMultiple, multipleItemsLimit }) => {
+ const initialState = {
+ isMultiple,
+ multipleItemsLimit,
+ items,
+ };
+ const [{ items: selectedItems }, dispatchSelectedItemsAction] = useReducer(selectedItemsReducer, initialState);
+
+ return { selectedItems, dispatchSelectedItemsAction };
+};
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/search.tab.module.js b/src/bundle/ui-dev/src/modules/universal-discovery/search.tab.module.js
index 226936e185..930afaa736 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/search.tab.module.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/search.tab.module.js
@@ -42,7 +42,7 @@ const SearchTabModule = () => {
return (
-
+
@@ -61,4 +61,4 @@ const SearchTab = {
isHiddenOnList: true,
};
-export { SearchTabModule as ValueTypeDefault, SearchTab };
+export { SearchTabModule as default, SearchTab };
diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/universal.discovery.module.js b/src/bundle/ui-dev/src/modules/universal-discovery/universal.discovery.module.js
index cec9ce9c2b..c54583d26a 100644
--- a/src/bundle/ui-dev/src/modules/universal-discovery/universal.discovery.module.js
+++ b/src/bundle/ui-dev/src/modules/universal-discovery/universal.discovery.module.js
@@ -23,9 +23,11 @@ import {
getTranslator,
SYSTEM_ROOT_LOCATION_ID,
} from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper';
+import { useSelectedItemsReducer } from './hooks/useSelectedItemsReducer';
const { document } = window;
const CLASS_SCROLL_DISABLED = 'ibexa-scroll-disabled';
+const SEARCH_TAB_ID = 'search';
const defaultRestInfo = {
accsessToken: null,
instanceUrl: window.location.origin,
@@ -178,6 +180,7 @@ export const TabsContext = createContext();
export const TitleContext = createContext();
export const CancelContext = createContext();
export const ConfirmContext = createContext();
+export const ConfirmItemsContext = createContext();
export const SortingContext = createContext();
export const SortOrderContext = createContext();
export const CurrentViewContext = createContext();
@@ -186,6 +189,7 @@ export const StartingLocationIdContext = createContext();
export const LoadedLocationsMapContext = createContext();
export const RootLocationIdContext = createContext();
export const SelectedLocationsContext = createContext();
+export const SelectedItemsContext = createContext();
export const CreateContentWidgetContext = createContext();
export const ContentOnTheFlyDataContext = createContext();
export const ContentOnTheFlyConfigContext = createContext();
@@ -196,6 +200,7 @@ export const DropdownPortalRefContext = createContext();
export const SuggestionsStorageContext = createContext();
export const GridActiveLocationIdContext = createContext();
export const SnackbarActionsContext = createContext();
+export const ViewContext = createContext();
const UniversalDiscoveryModule = (props) => {
const { restInfo } = props;
@@ -231,6 +236,10 @@ const UniversalDiscoveryModule = (props) => {
{ parentLocationId: props.rootLocationId, subitems: [] },
]);
const [selectedLocations, dispatchSelectedLocationsAction] = useSelectedLocationsReducer();
+ const { selectedItems, dispatchSelectedItemsAction } = useSelectedItemsReducer({
+ isMultiple: props.multiple,
+ multipleItemsLimit: props.multipleItemsLimit,
+ });
const activeTabConfig = tabs.find((tab) => tab.id === activeTab);
const Tab = activeTabConfig.component;
const className = createCssClassNames({
@@ -280,9 +289,9 @@ const UniversalDiscoveryModule = (props) => {
[adminUiConfig.contentTypes],
);
const onConfirm = useCallback(
- (selectedItems = selectedLocations) => {
+ (selection = selectedLocations) => {
loadVersions().then((locationsWithVersions) => {
- const clonedSelectedLocation = deepClone(selectedItems);
+ const clonedSelectedLocation = deepClone(selection);
if (Array.isArray(locationsWithVersions)) {
locationsWithVersions.forEach((content) => {
@@ -308,8 +317,19 @@ const UniversalDiscoveryModule = (props) => {
props.onConfirm(updatedLocations);
});
},
- [selectedLocations, contentTypesInfoMap],
+ [selectedLocations, contentTypesInfoMap, props.onConfirm],
+ );
+ const onItemsConfirm = useCallback(
+ (selection = selectedItems) => props.onItemsConfirm(selection),
+ [selectedItems, props.onItemsConfirm],
);
+ const makeSearch = (value) => {
+ if (activeTab !== SEARCH_TAB_ID) {
+ setActiveTab('search');
+ }
+
+ setSearchText(value);
+ };
useEffect(() => {
const addContentTypesInfo = (contentTypes) => {
@@ -487,100 +507,118 @@ const UniversalDiscoveryModule = (props) => {
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -605,6 +643,7 @@ const UniversalDiscoveryModule = (props) => {
UniversalDiscoveryModule.propTypes = {
onConfirm: PropTypes.func.isRequired,
+ onItemsConfirm: PropTypes.func,
onCancel: PropTypes.func,
title: PropTypes.string.isRequired,
activeTab: PropTypes.string,
@@ -645,6 +684,7 @@ UniversalDiscoveryModule.propTypes = {
};
UniversalDiscoveryModule.defaultProps = {
+ onItemsConfirm: () => {},
onCancel: null,
activeTab: 'browse',
rootLocationId: 1,