123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- <template>
- <NcModal id="unified-search"
- ref="unifiedSearchModal"
- :show.sync="internalIsVisible"
- :clear-view-delay="0"
- @close="closeModal">
- <CustomDateRangeModal :is-open="showDateRangeModal"
- class="unified-search__date-range"
- @set:custom-date-range="setCustomDateRange"
- @update:is-open="showDateRangeModal = $event" />
- <!-- Unified search form -->
- <div ref="unifiedSearch" class="unified-search-modal">
- <div class="unified-search-modal__header">
- <h2>{{ t('core', 'Unified search') }}</h2>
- <NcInputField ref="searchInput"
- data-cy-unified-search-input
- :value.sync="searchQuery"
- type="text"
- :label="t('core', 'Search apps, files, tags, messages') + '...'"
- @update:value="debouncedFind" />
- <div class="unified-search-modal__filters" data-cy-unified-search-filters>
- <NcActions :menu-name="t('core', 'Places')" :open.sync="providerActionMenuIsOpen" data-cy-unified-search-filter="places">
- <template #icon>
- <ListBox :size="20" />
- </template>
- <!-- Provider id's may be duplicated since, plugin filters could depend on a provider that is already in the defaults.
- provider.id concatenated to provider.name is used to create the item id, if same then, there should be an issue. -->
- <NcActionButton v-for="provider in providers"
- :key="`${provider.id}-${provider.name.replace(/\s/g, '')}`"
- @click="addProviderFilter(provider)">
- <template #icon>
- <img :src="provider.icon" class="filter-button__icon" alt="">
- </template>
- {{ provider.name }}
- </NcActionButton>
- </NcActions>
- <NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen" data-cy-unified-search-filter="date">
- <template #icon>
- <CalendarRangeIcon :size="20" />
- </template>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
- {{ t('core', 'Today') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
- {{ t('core', 'Last 7 days') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
- {{ t('core', 'Last 30 days') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
- {{ t('core', 'This year') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
- {{ t('core', 'Last year') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
- {{ t('core', 'Custom date range') }}
- </NcActionButton>
- </NcActions>
- <SearchableList :label-text="t('core', 'Search people')"
- :search-list="userContacts"
- :empty-content-text="t('core', 'Not found')"
- data-cy-unified-search-filter="people"
- @search-term-change="debouncedFilterContacts"
- @item-selected="applyPersonFilter">
- <template #trigger>
- <NcButton>
- <template #icon>
- <AccountGroup :size="20" />
- </template>
- {{ t('core', 'People') }}
- </NcButton>
- </template>
- </SearchableList>
- <NcButton v-if="supportFiltering" data-cy-unified-search-filter="current-view" @click="closeModal">
- {{ t('core', 'Filter in current view') }}
- <template #icon>
- <FilterIcon :size="20" />
- </template>
- </NcButton>
- </div>
- <div class="unified-search-modal__filters-applied">
- <FilterChip v-for="filter in filters"
- :key="filter.id"
- :text="filter.name ?? filter.text"
- :pretext="''"
- @delete="removeFilter(filter)">
- <template #icon>
- <NcAvatar v-if="filter.type === 'person'"
- :user="filter.user"
- :size="24"
- :disable-menu="true"
- :show-user-status="false"
- :hide-favorite="false" />
- <CalendarRangeIcon v-else-if="filter.type === 'date'" />
- <img v-else :src="filter.icon" alt="">
- </template>
- </FilterChip>
- </div>
- </div>
- <div v-if="noContentInfo.show" class="unified-search-modal__no-content">
- <NcEmptyContent :name="noContentInfo.text">
- <template #icon>
- <component :is="noContentInfo.icon" />
- </template>
- </NcEmptyContent>
- </div>
- <div v-else class="unified-search-modal__results">
- <div v-for="providerResult in results" :key="providerResult.id" class="result">
- <div class="result-title">
- <span>{{ providerResult.provider }}</span>
- </div>
- <ul class="result-items">
- <SearchResult v-for="(result, index) in providerResult.results" :key="index" v-bind="result" />
- </ul>
- <div class="result-footer">
- <NcButton type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult.id)">
- {{ t('core', 'Load more results') }}
- <template #icon>
- <DotsHorizontalIcon :size="20" />
- </template>
- </NcButton>
- <NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" type="tertiary-no-background">
- {{ t('core', 'Search in') }} {{ providerResult.provider }}
- <template #icon>
- <ArrowRight :size="20" />
- </template>
- </NcButton>
- </div>
- </div>
- </div>
- </div>
- </NcModal>
- </template>
-
- <script>
- import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
- import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
- import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue'
- import CustomDateRangeModal from '../components/UnifiedSearch/CustomDateRangeModal.vue'
- import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
- import FilterIcon from 'vue-material-design-icons/Filter.vue'
- import FilterChip from '../components/UnifiedSearch/SearchFilterChip.vue'
- import ListBox from 'vue-material-design-icons/ListBox.vue'
- import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
- import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
- import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
- import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
- import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
- import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
- import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
- import MagnifyIcon from 'vue-material-design-icons/Magnify.vue'
- import SearchableList from '../components/UnifiedSearch/SearchableList.vue'
- import SearchResult from '../components/UnifiedSearch/SearchResult.vue'
-
- import debounce from 'debounce'
- import { emit, subscribe } from '@nextcloud/event-bus'
- import { useBrowserLocation } from '@vueuse/core'
- import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'
- import { useSearchStore } from '../store/unified-search-external-filters.js'
-
- export default {
- name: 'UnifiedSearchModal',
- components: {
- ArrowRight,
- AccountGroup,
- CalendarRangeIcon,
- CustomDateRangeModal,
- DotsHorizontalIcon,
- FilterIcon,
- FilterChip,
- ListBox,
- NcActions,
- NcActionButton,
- NcAvatar,
- NcButton,
- NcEmptyContent,
- NcModal,
- NcInputField,
- MagnifyIcon,
- SearchableList,
- SearchResult,
- },
- props: {
- isVisible: {
- type: Boolean,
- required: true,
- },
- },
- setup() {
- /**
- * Reactive version of window.location
- */
- const currentLocation = useBrowserLocation()
- const searchStore = useSearchStore()
- return {
- currentLocation,
- externalFilters: searchStore.externalFilters,
- }
- },
- data() {
- return {
- providers: [],
- providerActionMenuIsOpen: false,
- dateActionMenuIsOpen: false,
- providerResultLimit: 5,
- dateFilter: { id: 'date', type: 'date', text: '', startFrom: null, endAt: null },
- personFilter: { id: 'person', type: 'person', name: '' },
- dateFilterIsApplied: false,
- personFilterIsApplied: false,
- filteredProviders: [],
- searching: false,
- searchQuery: '',
- placessearchTerm: '',
- dateTimeFilter: null,
- filters: [],
- results: [],
- contacts: [],
- debouncedFind: debounce(this.find, 300),
- debouncedFilterContacts: debounce(this.filterContacts, 300),
- showDateRangeModal: false,
- internalIsVisible: false,
- }
- },
-
- computed: {
- userContacts() {
- return this.contacts
- },
- noContentInfo() {
- const isEmptySearch = this.searchQuery.length === 0
- const hasNoResults = this.searchQuery.length > 0 && this.results.length === 0
- return {
- show: isEmptySearch || hasNoResults,
- text: this.searching && hasNoResults ? t('core', 'Searching …') : (isEmptySearch ? t('core', 'Start typing to search') : t('core', 'No matching results')),
- icon: MagnifyIcon,
- }
- },
- supportFiltering() {
- /* Hard coded apps for the moment this would be improved in coming updates. */
- const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
- return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
- },
- },
- watch: {
- isVisible(value) {
- if (value) {
- /*
- * Before setting the search UI to visible, reset previous search event emissions.
- * This allows apps to restore defaults after "Filter in current view" if the user opens the search interface once more.
- * Additionally, it's a new search, so it's better to reset all previous events emitted.
- */
- emit('nextcloud:unified-search.reset', { query: '' })
- }
- this.internalIsVisible = value
- },
- internalIsVisible(value) {
- this.$emit('update:isVisible', value)
- this.$nextTick(() => {
- if (value) {
- this.focusInput()
- }
- })
- },
-
- },
- mounted() {
- subscribe('nextcloud:unified-search:add-filter', this.handlePluginFilter)
- getProviders().then((providers) => {
- this.providers = providers
- this.externalFilters.forEach(filter => {
- this.providers.push(filter)
- })
- this.providers = this.groupProvidersByApp(this.providers)
- console.debug('Search providers', this.providers)
- })
- getContacts({ searchTerm: '' }).then((contacts) => {
- this.contacts = this.mapContacts(contacts)
- console.debug('Contacts', this.contacts)
- })
- },
- methods: {
- find(query) {
- this.searching = true
- if (query.length === 0) {
- this.results = []
- this.searching = false
- emit('nextcloud:unified-search.reset', { query })
- return
- }
- emit('nextcloud:unified-search.search', { query })
- const newResults = []
- const providersToSearch = this.filteredProviders.length > 0 ? this.filteredProviders : this.providers
- const searchProvider = (provider, filters) => {
- const params = {
- type: provider.id,
- query,
- cursor: null,
- extraQueries: provider.extraParams,
- }
-
- if (filters.dateFilterIsApplied) {
- if (provider.filters.since && provider.filters.until) {
- params.since = this.dateFilter.startFrom
- params.until = this.dateFilter.endAt
- } else {
- // Date filter is applied but provider does not support it, no need to search provider
- return
- }
- }
-
- if (filters.personFilterIsApplied) {
- if (provider.filters.person) {
- params.person = this.personFilter.user
- } else {
- // Person filter is applied but provider does not support it, no need to search provider
- return
- }
- }
-
- if (this.providerResultLimit > 5) {
- params.limit = this.providerResultLimit
- }
-
- const request = unifiedSearch(params).request
-
- request().then((response) => {
- newResults.push({
- id: provider.id,
- provider: provider.name,
- inAppSearch: provider.inAppSearch,
- results: response.data.ocs.data.entries,
- })
-
- console.debug('New results', newResults)
- console.debug('Unified search results:', this.results)
-
- this.updateResults(newResults)
- this.searching = false
- })
- }
- providersToSearch.forEach(provider => {
- const dateFilterIsApplied = this.dateFilterIsApplied
- const personFilterIsApplied = this.personFilterIsApplied
- searchProvider(provider, { dateFilterIsApplied, personFilterIsApplied })
- })
-
- },
- updateResults(newResults) {
- let updatedResults = [...this.results]
- // If filters are applied, remove any previous results for providers that are not in current filters
- if (this.filters.length > 0) {
- updatedResults = updatedResults.filter(result => {
- return this.filters.some(filter => filter.id === result.id)
- })
- }
- // Process the new results
- newResults.forEach(newResult => {
- const existingResultIndex = updatedResults.findIndex(result => result.id === newResult.id)
- if (existingResultIndex !== -1) {
- if (newResult.results.length === 0) {
- // If the new results data has no matches for and existing result, remove the existing result
- updatedResults.splice(existingResultIndex, 1)
- } else {
- // If input triggered a change in existing results, update existing result
- updatedResults.splice(existingResultIndex, 1, newResult)
- }
- } else if (newResult.results.length > 0) {
- // Push the new result to the array only if its results array is not empty
- updatedResults.push(newResult)
- }
- })
- const sortedResults = updatedResults.slice(0)
- // Order results according to provider preference
- sortedResults.sort((a, b) => {
- const aProvider = this.providers.find(provider => provider.id === a.id)
- const bProvider = this.providers.find(provider => provider.id === b.id)
- const aOrder = aProvider ? aProvider.order : 0
- const bOrder = bProvider ? bProvider.order : 0
- return aOrder - bOrder
- })
- this.results = sortedResults
- },
- mapContacts(contacts) {
- return contacts.map(contact => {
- return {
- // id: contact.id,
- // name: '',
- displayName: contact.fullName,
- isNoUser: false,
- subname: contact.emailAddresses[0] ? contact.emailAddresses[0] : '',
- icon: '',
- user: contact.id,
- }
- })
- },
- filterContacts(query) {
- getContacts({ searchTerm: query }).then((contacts) => {
- this.contacts = this.mapContacts(contacts)
- console.debug(`Contacts filtered by ${query}`, this.contacts)
- })
- },
- applyPersonFilter(person) {
- this.personFilterIsApplied = true
- const existingPersonFilter = this.filters.findIndex(filter => filter.id === person.id)
- if (existingPersonFilter === -1) {
- this.personFilter.id = person.id
- this.personFilter.user = person.user
- this.personFilter.name = person.displayName
- this.filters.push(this.personFilter)
- } else {
- this.filters[existingPersonFilter].id = person.id
- this.filters[existingPersonFilter].user = person.user
- this.filters[existingPersonFilter].name = person.displayName
- }
-
- this.debouncedFind(this.searchQuery)
- console.debug('Person filter applied', person)
- },
- loadMoreResultsForProvider(providerId) {
- this.providerResultLimit += 5
- this.filters = this.filters.filter(filter => filter.type !== 'provider')
- const provider = this.providers.find(provider => provider.id === providerId)
- this.addProviderFilter(provider, true)
- },
- addProviderFilter(providerFilter, loadMoreResultsForProvider = false) {
- if (!providerFilter.id) return
- if (providerFilter.isPluginFilter) {
- providerFilter.callback()
- }
- this.providerResultLimit = loadMoreResultsForProvider ? this.providerResultLimit : 5
- this.providerActionMenuIsOpen = false
- // With the possibility for other apps to add new filters
- // Resulting in a possible id/provider collision
- // If a user tries to apply a filter that seems to already exist, we remove the current one and add the new one.
- const existingFilterIndex = this.filteredProviders.findIndex(existing => existing.id === providerFilter.id)
- if (existingFilterIndex > -1) {
- this.filteredProviders.splice(existingFilterIndex, 1)
- this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
- }
- this.filteredProviders.push({
- id: providerFilter.id,
- name: providerFilter.name,
- icon: providerFilter.icon,
- type: providerFilter.type || 'provider',
- filters: providerFilter.filters,
- isPluginFilter: providerFilter.isPluginFilter || false,
- })
- this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
- console.debug('Search filters (newly added)', this.filters)
- this.debouncedFind(this.searchQuery)
- },
- removeFilter(filter) {
- if (filter.type === 'provider') {
- for (let i = 0; i < this.filteredProviders.length; i++) {
- if (this.filteredProviders[i].id === filter.id) {
- this.filteredProviders.splice(i, 1)
- break
- }
- }
- this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
- console.debug('Search filters (recently removed)', this.filters)
-
- } else {
- for (let i = 0; i < this.filters.length; i++) {
- // Remove date and person filter
- if (this.filters[i].id === 'date' || this.filters[i].id === filter.id) {
- this.dateFilterIsApplied = false
- this.filters.splice(i, 1)
- if (filter.type === 'person') {
- this.personFilterIsApplied = false
- }
- break
- }
- }
- }
- this.debouncedFind(this.searchQuery)
- },
- syncProviderFilters(firstArray, secondArray) {
- // Create a copy of the first array to avoid modifying it directly.
- const synchronizedArray = firstArray.slice()
- // Remove items from the synchronizedArray that are not in the secondArray.
- synchronizedArray.forEach((item, index) => {
- const itemId = item.id
- if (item.type === 'provider') {
- if (!secondArray.some(secondItem => secondItem.id === itemId)) {
- synchronizedArray.splice(index, 1)
- }
- }
- })
- // Add items to the synchronizedArray that are in the secondArray but not in the firstArray.
- secondArray.forEach(secondItem => {
- const itemId = secondItem.id
- if (secondItem.type === 'provider') {
- if (!synchronizedArray.some(item => item.id === itemId)) {
- synchronizedArray.push(secondItem)
- }
- }
- })
-
- return synchronizedArray
- },
- updateDateFilter() {
- const currFilterIndex = this.filters.findIndex(filter => filter.id === 'date')
- if (currFilterIndex !== -1) {
- this.filters[currFilterIndex] = this.dateFilter
- } else {
- this.filters.push(this.dateFilter)
- }
- this.dateFilterIsApplied = true
- this.debouncedFind(this.searchQuery)
- },
- applyQuickDateRange(range) {
- this.dateActionMenuIsOpen = false
- const today = new Date()
- let startDate
- let endDate
-
- switch (range) {
- case 'today':
- // For 'Today', both start and end are set to today
- startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0)
- endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999)
- this.dateFilter.text = t('core', 'Today')
- break
- case '7days':
- // For 'Last 7 days', start date is 7 days ago, end is today
- startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6, 0, 0, 0, 0)
- this.dateFilter.text = t('core', 'Last 7 days')
- break
- case '30days':
- // For 'Last 30 days', start date is 30 days ago, end is today
- startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29, 0, 0, 0, 0)
- this.dateFilter.text = t('core', 'Last 30 days')
- break
- case 'thisyear':
- // For 'This year', start date is the first day of the year, end is the last day of the year
- startDate = new Date(today.getFullYear(), 0, 1, 0, 0, 0, 0)
- endDate = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999)
- this.dateFilter.text = t('core', 'This year')
- break
- case 'lastyear':
- // For 'Last year', start date is the first day of the previous year, end is the last day of the previous year
- startDate = new Date(today.getFullYear() - 1, 0, 1, 0, 0, 0, 0)
- endDate = new Date(today.getFullYear() - 1, 11, 31, 23, 59, 59, 999)
- this.dateFilter.text = t('core', 'Last year')
- break
- case 'custom':
- this.showDateRangeModal = true
- return
- default:
- return
- }
- this.dateFilter.startFrom = startDate
- this.dateFilter.endAt = endDate
- this.updateDateFilter()
-
- },
- setCustomDateRange(event) {
- console.debug('Custom date range', event)
- this.dateFilter.startFrom = event.startFrom
- this.dateFilter.endAt = event.endAt
- this.dateFilter.text = t('core', `Between ${this.dateFilter.startFrom.toLocaleDateString()} and ${this.dateFilter.endAt.toLocaleDateString()}`)
- this.updateDateFilter()
- },
- handlePluginFilter(addFilterEvent) {
- for (let i = 0; i < this.filteredProviders.length; i++) {
- const provider = this.filteredProviders[i]
- if (provider.id === addFilterEvent.id) {
- provider.name = addFilterEvent.filterUpdateText
- // Filters attached may only make sense with certain providers,
- // So, find the provider attached, add apply the extra parameters to those providers only
- const compatibleProviderIndex = this.providers.findIndex(provider => provider.id === addFilterEvent.id)
- if (compatibleProviderIndex > -1) {
- provider.extraParams = addFilterEvent.filterParams
- this.filteredProviders[i] = provider
- }
- break
- }
- }
- this.debouncedFind(this.searchQuery)
- },
- groupProvidersByApp(filters) {
- const groupedByProviderApp = {}
-
- filters.forEach(filter => {
- const provider = filter.appId ? filter.appId : 'general'
- if (!groupedByProviderApp[provider]) {
- groupedByProviderApp[provider] = []
- }
- groupedByProviderApp[provider].push(filter)
- })
-
- const flattenedArray = []
- Object.values(groupedByProviderApp).forEach(group => {
- flattenedArray.push(...group)
- })
-
- return flattenedArray
- },
- focusInput() {
- this.$refs.searchInput.$el.children[0].children[0].focus()
- },
- closeModal() {
- this.internalIsVisible = false
- this.searchQuery = ''
- },
- },
- }
- </script>
-
- <style lang="scss" scoped>
- .unified-search-modal {
- box-sizing: border-box;
- height: 100%;
- min-height: 80vh;
-
- display: flex;
- flex-direction: column;
- padding-block: 10px 0;
-
- // inline padding on direct children to make sure the scrollbar is on the modal container
- >* {
- padding-inline: 20px;
- }
-
- &__header {
- padding-block-end: 8px;
- }
-
- &__heading {
- font-size: 16px;
- font-weight: bolder;
- line-height: 2em;
- margin-bottom: 0;
- }
-
- &__filters {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- justify-content: start;
- padding-top: 4px;
- }
-
- &__filters-applied {
- padding-top: 4px;
- display: flex;
- flex-wrap: wrap;
- }
-
- &__no-content {
- display: flex;
- align-items: center;
- height: 100%;
- }
-
- &__results {
- overflow: hidden scroll;
- padding-block: 0 10px;
-
- .result {
- &-title {
- span {
- color: var(--color-primary-element);
- font-weight: bolder;
- font-size: 16px;
- }
- }
-
- &-footer {
- justify-content: space-between;
- align-items: center;
- display: flex;
- }
- }
-
- }
- }
-
- .filter-button__icon {
- height: 20px;
- width: 20px;
- object-fit: contain;
- filter: var(--background-invert-if-bright);
- padding: 11px; // align with text to fit at least 44px
- }
-
- // Ensure modal is accessible on small devices
- @media only screen and (max-height: 400px) {
- .unified-search-modal__results {
- overflow: unset;
- }
- }
- </style>
|