From 62e0b6bc881541d5352ac342b5adb649f8072105 Mon Sep 17 00:00:00 2001 From: tischsoic Date: Fri, 22 Nov 2024 11:42:00 +0100 Subject: [PATCH] IBX-9069: Initial Product Tab --- .../scss/ui/modules/_universal.discovery.scss | 2 + .../modules/universal-discovery/_search.scss | 18 +- .../_selected.items.panel.item.scss | 56 ++++++ .../_selected.items.panel.scss | 75 +++++++++ .../_selected.locations.scss | 37 +++- .../components/filters/filters.js | 43 ++--- .../components/filters/filters.panel.js | 42 +++++ .../components/filters/filters.row.js | 26 +++ .../components/search/search.js | 10 +- .../selected.items.panel.item.js | 71 ++++++++ .../selected-items/selected.items.panel.js | 152 +++++++++++++++++ .../selected-locations/selected.locations.js | 10 +- .../components/sort-switcher/sort.switcher.js | 22 ++- .../universal-discovery/components/tab/tab.js | 20 ++- .../toggle-selection/toggle.item.selection.js | 28 +++ .../components/top-menu/top.menu.js | 6 +- .../top-menu/top.menu.search.input.js | 17 +- .../components/view-switcher/view.switcher.js | 7 +- .../hooks/usePaginableFetch.js | 63 +++++++ .../hooks/useSelectedItemsReducer.js | 77 +++++++++ .../universal-discovery/search.tab.module.js | 4 +- .../universal.discovery.module.js | 159 +++++++++++------- .../Component/UniversalDiscoveryWidget.php | 2 +- 23 files changed, 793 insertions(+), 154 deletions(-) create mode 100644 src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.items.panel.item.scss create mode 100644 src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.items.panel.scss create mode 100644 src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.panel.js create mode 100644 src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.row.js create mode 100644 src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.item.js create mode 100644 src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.js create mode 100644 src/bundle/ui-dev/src/modules/universal-discovery/components/toggle-selection/toggle.item.selection.js create mode 100644 src/bundle/ui-dev/src/modules/universal-discovery/hooks/usePaginableFetch.js create mode 100644 src/bundle/ui-dev/src/modules/universal-discovery/hooks/useSelectedItemsReducer.js diff --git a/src/bundle/Resources/public/scss/ui/modules/_universal.discovery.scss b/src/bundle/Resources/public/scss/ui/modules/_universal.discovery.scss index 022c324534..264ea71e46 100644 --- a/src/bundle/Resources/public/scss/ui/modules/_universal.discovery.scss +++ b/src/bundle/Resources/public/scss/ui/modules/_universal.discovery.scss @@ -15,6 +15,8 @@ @import 'universal-discovery/finder.branch'; @import 'universal-discovery/finder.leaf'; @import 'universal-discovery/content.meta.preview'; +@import 'universal-discovery/selected.items.panel.item'; +@import 'universal-discovery/selected.items.panel'; @import 'universal-discovery/selected.locations'; @import 'universal-discovery/selected.locations.item'; @import 'universal-discovery/grid'; diff --git a/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_search.scss b/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_search.scss index 102cc9048f..ed8dc3ce20 100644 --- a/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_search.scss +++ b/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_search.scss @@ -19,13 +19,15 @@ height: 100%; } - &__sidebar { - display: flex; + &__content-meta-preview:not(:empty) { + flex: 1; height: 100%; - min-width: calculateRem(270px); - margin-right: calculateRem(24px); - border-right: calculateRem(1px) solid $ibexa-color-light; - background-color: $ibexa-color-white; + overflow: auto; + border-left: calculateRem(1px) solid $ibexa-color-light; + } + + &__filters { + border-left: calculateRem(1px) solid $ibexa-color-light; } &__spinner-wrapper { @@ -41,12 +43,13 @@ } &__content { + flex: 2; display: flex; flex-direction: column; overflow: auto; width: 100%; flex-shrink: 1; - padding: 0 calculateRem(8px); + padding: calculateRem(24px); background-color: $ibexa-color-white; position: relative; } @@ -73,7 +76,6 @@ display: grid; grid-template: 'title clear-search-btn' 'subtitle subtitle'; justify-content: start; - margin-top: calculateRem(16px); } &__table-tile { diff --git a/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.items.panel.item.scss b/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.items.panel.item.scss new file mode 100644 index 0000000000..7e48caf675 --- /dev/null +++ b/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.items.panel.item.scss @@ -0,0 +1,56 @@ +.c-selected-items-panel-item { + display: flex; + align-items: center; + padding: calculateRem(5px); + margin-bottom: calculateRem(8px); + border: calculateRem(1px) solid $ibexa-color-light; + border-radius: $ibexa-border-radius; + box-shadow: calculateRem(4px) calculateRem(2px) calculateRem(17px) 0 rgba($ibexa-color-black, 0.05); + + &__image-wrapper { + width: calculateRem(42px); + height: calculateRem(42px); + background-color: $ibexa-color-light-300; + border-radius: $ibexa-border-radius-small; + display: flex; + justify-content: center; + align-items: center; + } + + &__info { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + width: calculateRem(185px); + padding: 0 calculateRem(15px); + } + + &__info-name { + font-size: $ibexa-text-font-size; + } + + &__info-description { + font-size: $ibexa-text-font-size-small; + color: $ibexa-color-dark-400; + } + + &__actions-wrapper { + display: flex; + justify-content: flex-end; + align-items: center; + } + + &__remove-button { + width: calculateRem(32px); + height: calculateRem(32px); + padding: 0; + display: inline-flex; + justify-content: center; + align-items: center; + + .ibexa-icon { + fill: $ibexa-color-dark; + } + } +} diff --git a/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.items.panel.scss b/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.items.panel.scss new file mode 100644 index 0000000000..647d12b53e --- /dev/null +++ b/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.items.panel.scss @@ -0,0 +1,75 @@ +.c-selected-items-panel { + background-color: $ibexa-color-white; + position: fixed; + top: calc(100vh - calculateRem(98px)); + bottom: calculateRem(31px); + left: calculateRem(16px); + min-height: calculateRem(60px); + border: calculateRem(1px) solid $ibexa-color-light; + border-top-right-radius: $ibexa-border-radius; + border-bottom-right-radius: $ibexa-border-radius; + box-shadow: calculateRem(4px) calculateRem(22px) calculateRem(47px) 0 rgba($ibexa-color-info, 0.15); + z-index: 1; + transition: all $ibexa-admin-transition-duration $ibexa-admin-transition; + + &__header { + display: flex; + justify-content: start; + align-items: center; + padding: calculateRem(16px); + } + + &__selection-counter { + color: $ibexa-color-dark; + font-size: calculateRem(22px); + font-weight: 600; + padding-right: calculateRem(16px); + } + + &--expanded { + bottom: calculateRem(16px); + top: calculateRem(88px); + min-width: calculateRem(491px); + overflow: hidden; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: $ibexa-border-radius; + + .c-selected-items-panel { + &__items-wrapper { + display: block; + } + + &__toggle-button-icon { + transform: rotate(0); + } + } + } + + &__items-wrapper { + display: none; + overflow: auto; + padding: 0 calculateRem(38px) calculateRem(16px) calculateRem(22px); + border-top: calculateRem(1px) solid $ibexa-color-light; + height: calc(100% - calculateRem(70px)); + } + + &__actions { + padding: calculateRem(16px) 0; + display: flex; + justify-content: flex-end; + } + + &__toggle-button { + display: flex; + width: calculateRem(32px); + height: calculateRem(32px); + justify-content: center; + align-items: center; + margin-right: calculateRem(32px); + } + + &__toggle-button-icon { + transform: rotate(180deg); + } +} diff --git a/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.locations.scss b/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.locations.scss index f4f33d1e42..06401f0910 100644 --- a/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.locations.scss +++ b/src/bundle/Resources/public/scss/ui/modules/universal-discovery/_selected.locations.scss @@ -1,22 +1,22 @@ .c-selected-locations { background-color: $ibexa-color-white; position: fixed; - top: calculateRem(95px); - right: calculateRem(16px); + top: calc(100vh - calculateRem(98px)); + bottom: calculateRem(31px); + left: calculateRem(16px); min-height: calculateRem(60px); - min-width: calculateRem(390px); border: calculateRem(1px) solid $ibexa-color-light; - border-top-left-radius: $ibexa-border-radius; - border-bottom-left-radius: $ibexa-border-radius; + border-top-right-radius: $ibexa-border-radius; + border-bottom-right-radius: $ibexa-border-radius; box-shadow: calculateRem(4px) calculateRem(22px) calculateRem(47px) 0 rgba($ibexa-color-info, 0.15); z-index: 1; transition: all $ibexa-admin-transition-duration $ibexa-admin-transition; &__header { display: flex; - justify-content: space-between; + justify-content: start; align-items: center; - padding: calculateRem(7px) calculateRem(12px) calculateRem(7px) calculateRem(22px); + padding: calculateRem(16px); } &__selection-counter { @@ -27,14 +27,22 @@ } &--expanded { - bottom: calculateRem(100px); + bottom: calculateRem(16px); + top: calculateRem(88px); min-width: calculateRem(491px); overflow: hidden; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: $ibexa-border-radius; .c-selected-locations { &__items-wrapper { display: block; } + + &__toggle-button-icon { + transform: rotate(0); + } } } @@ -51,4 +59,17 @@ display: flex; justify-content: flex-end; } + + &__toggle-button { + display: flex; + width: calculateRem(32px); + height: calculateRem(32px); + justify-content: center; + align-items: center; + margin-right: calculateRem(32px); + } + + &__toggle-button-icon { + transform: rotate(180deg); + } } diff --git a/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.js b/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.js index 062add5f88..cf7fac210e 100644 --- a/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.js +++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.js @@ -2,6 +2,9 @@ import React, { useContext, useState, useEffect, useCallback, useRef } from 'rea import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; +import FiltersPanel from './filters.panel'; +import FiltersRow from './filters.row'; + import { SelectedContentTypesContext, SelectedSectionContext, @@ -69,7 +72,7 @@ const Filters = ({ search }) => { 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..9d43101d5a --- /dev/null +++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.panel.js @@ -0,0 +1,42 @@ +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.isRequired, + isApplyButtonEnabled: PropTypes.bool.isRequired, + makeSearch: PropTypes.func.isRequired, + clearFilters: PropTypes.func.isRequired, +}; + +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..4beff1df96 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/filters/filters.row.js @@ -0,0 +1,26 @@ +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 ( +
+
{title}
+ {children} +
+ ); +}; + +FiltersRow.propTypes = { + children: PropTypes.node.isRequired, + extraClasses: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, +}; + +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 }) => {
-
- -
{renderSearchResults()}
+
+ +
+
+ +
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..e593de19a7 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.item.js @@ -0,0 +1,71 @@ +import React, { useContext, useEffect, 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 Icon from '../../../common/icon/icon'; +import Thumbnail from '../../../common/thumbnail/thumbnail'; + +import { SelectedItemsContext } from '../../universal.discovery.module'; +import { getAdminUiConfig, getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +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; + }); + }, []); + + useEffect(() => { + parseTooltip(refSelectedLocationsItem.current); + }, []); + + return ( +
+
+ +
+
+ {name} + {description} +
+
+ {sortedActions.map((action) => { + const Component = action.component; + + return ; + })} + +
+
+ ); +}; + +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..00050d6ed8 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/selected-items/selected.items.panel.js @@ -0,0 +1,152 @@ +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(); + // TODO: fill dependency array + 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_locations.expand.sidebar', + {}, + 'ibexa_universal_discovery_widget', + ); + const collapseLabel = Translator.trans( + /*@Desc("Collapse sidebar")*/ 'selected_locations.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_locations.selected_items', + selectedItems.length, + { count: selectedItems.length }, + 'ibexa_universal_discovery_widget', + ); + + return
{selectedLabel}
; + }; + const renderToggleButton = () => { + return ( + + ); + }; + const renderActionButtons = () => { + const removeLabel = Translator.transChoice( + /*@Desc("{1}Deselect|[2,Inf]Deselect all")*/ 'selected_locations.deselect_all', + selectedItems.length, + {}, + 'ibexa_universal_discovery_widget', + ); + + return ( +
+ +
+ ); + }; + const renderLocationsList = () => { + if (!isExpanded) { + return null; + } + + return ( +
+ {renderActionButtons()} +
+ {selectedItems.map((selectedItem) => { + const ItemComponent = itemsComponentsMap[selectedItem.type].component; + + return ItemComponent && ; + })} +
+
+ ); + }; + + useEffect(() => { + if (!allowConfirmation) { + return; + } + + parseTooltip(refSelectedLocations.current); + hideAllTooltips(); + + const bootstrap = getBootstrap(); + const toggleButtonTooltip = bootstrap.Tooltip.getOrCreateInstance('.c-selected-items-panel__toggle-button'); + + toggleButtonTooltip.setContent({ '.tooltip-inner': togglerLabel }); + }, [isExpanded]); + + if (!allowConfirmation) { + return null; + } + + return ( +
+
+ {renderToggleButton()} + {renderSelectionCounter()} +
+ {renderLocationsList()} +
+ ); +}; + +export default SelectedItemsPanel; 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..e41a5580b3 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 = false, disabledConfig = null }) => { const [sorting, setSorting] = useContext(SortingContext); const [sortOrder, setSortOrder] = useContext(SortOrderContext); const selectedOption = SORTING_OPTIONS.find((option) => option.sortClause === sorting && option.sortOrder === sortOrder); @@ -13,8 +15,19 @@ const SortSwitcher = ({ isDisabled }) => { 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, -}; - -SortSwitcher.defaultProps = { - isDisabled: false, + disabledConfig: PropTypes.object, }; 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..85e93d62dd 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..b58a477ce2 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/universal-discovery/components/toggle-selection/toggle.item.selection.js @@ -0,0 +1,28 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; + +import { createCssClassNames } from '../../../common/helpers/css.class.names'; +import { SelectedItemsContext } from '../../universal.discovery.module'; +import { TOGGLE_SELECTED_ITEMS } from '../../hooks/useSelectedItemsReducer'; + +const ToggleItemSelection = ({ multiple, item, isHidden = false }) => { + const { selectedItems, dispatchSelectedItemsAction } = useContext(SelectedItemsContext); + const isSelected = selectedItems.some((selectedItem) => selectedItem.type === item.type && selectedItem.id === item.id); + const className = createCssClassNames({ + 'c-udw-toggle-selection ibexa-input': true, + 'ibexa-input--checkbox': multiple, + 'c-udw-toggle-selection--hidden': isHidden, + }); + const inputType = multiple ? 'checkbox' : 'radio'; + + console.log(isSelected, selectedItems, item.type, item.id); + return ; +}; + +ToggleItemSelection.propTypes = { + item: PropTypes.object.isRequired, + multiple: PropTypes.bool.isRequired, + isHidden: PropTypes.bool, +}; + +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..6df58e9c82 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, setSearchText, 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..1b230c878d --- /dev/null +++ b/src/bundle/ui-dev/src/modules/universal-discovery/hooks/useSelectedItemsReducer.js @@ -0,0 +1,77 @@ +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 selectedItemsReducer = (state, action) => { + const { isMultiple, items } = state; + + switch (action.type) { + case ADD_SELECTED_ITEMS: { + const oldItemsWithoutNewItems = filterOutSelectedItems(action.items, items); + const newItems = [...oldItemsWithoutNewItems, ...action.items]; + + if (!isMultiple && newItems.length > 1) { + 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 (!isMultiple && newItems.length > 1) { + 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..92827f0e66 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, @@ -186,6 +188,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 +199,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 +235,10 @@ const UniversalDiscoveryModule = (props) => { { parentLocationId: props.rootLocationId, subitems: [] }, ]); const [selectedLocations, dispatchSelectedLocationsAction] = useSelectedLocationsReducer(); + const { selectedItems, dispatchSelectedItemsAction } = useSelectedItemsReducer({ + isMultiple: true,//props.multiple, + multipleItemsLimit: props.multipleItemsLimit, + }); const activeTabConfig = tabs.find((tab) => tab.id === activeTab); const Tab = activeTabConfig.component; const className = createCssClassNames({ @@ -310,6 +318,13 @@ const UniversalDiscoveryModule = (props) => { }, [selectedLocations, contentTypesInfoMap], ); + const makeSearch = (value) => { + if (activeTab !== SEARCH_TAB_ID) { + setActiveTab('search'); + } + + setSearchText(value); + }; useEffect(() => { const addContentTypesInfo = (contentTypes) => { @@ -475,7 +490,7 @@ const UniversalDiscoveryModule = (props) => { - + @@ -494,90 +509,106 @@ const UniversalDiscoveryModule = (props) => { - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/lib/Behat/Component/UniversalDiscoveryWidget.php b/src/lib/Behat/Component/UniversalDiscoveryWidget.php index 28585e6dea..f44aa40206 100644 --- a/src/lib/Behat/Component/UniversalDiscoveryWidget.php +++ b/src/lib/Behat/Component/UniversalDiscoveryWidget.php @@ -218,7 +218,7 @@ protected function specifyLocators(): array new CSSLocator('confirmButton', '.c-actions-menu__confirm-btn'), new CSSLocator('cancelButton', '.c-top-menu__cancel-btn'), new CSSLocator('mainWindow', '.m-ud'), - new CSSLocator('selectedLocationsTab', '.c-selected-locations'), + new CSSLocator('selectedLocationsTab', '.c-selected-items-panel'), new CSSLocator('categoryTabSelector', '.c-tab-selector__item'), new CSSLocator('selectedTab', '.c-tab-selector__item--selected'), new VisibleCSSLocator('contentIframe', '.c-content-edit__iframe, .m-content-create__iframe'),