Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable28] enh(UnifiedSearch): Keep the searchbar on top of the modal #42776

Merged
merged 2 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 140 additions & 128 deletions core/src/views/UnifiedSearchModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,79 +10,89 @@
@update:is-open="showDateRangeModal = $event" />
<!-- Unified search form -->
<div ref="unifiedSearch" class="unified-search-modal">
<h2>{{ t('core', 'Unified search') }}</h2>
<NcInputField ref="searchInput"
:value.sync="searchQuery"
type="text"
:label="t('core', 'Search apps, files, tags, messages') + '...'"
@update:value="debouncedFind" />
<div class="unified-search-modal__filters">
<NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen">
<template #icon>
<ListBox :size="20" />
</template>
<NcActionButton v-for="provider in providers" :key="provider.id" @click="addProviderFilter(provider)">
<div class="unified-search-modal__header">
<h2>{{ t('core', 'Unified search') }}</h2>
<NcInputField ref="searchInput"
:value.sync="searchQuery"
type="text"
:label="t('core', 'Search apps, files, tags, messages') + '...'"
@update:value="debouncedFind" />
<div class="unified-search-modal__filters">
<NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen">
<template #icon>
<img :src="provider.icon">
<ListBox :size="20" />
</template>
{{ t('core', provider.name) }}
</NcActionButton>
</NcActions>
<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen">
<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')"
@search-term-change="debouncedFilterContacts"
@item-selected="applyPersonFilter">
<template #trigger>
<NcButton>
<NcActionButton v-for="provider in providers"
:key="provider.id"
@click="addProviderFilter(provider)">
<template #icon>
<AccountGroup :size="20" />
<img :src="provider.icon" class="filter-button__icon" alt="">
</template>
{{ t('core', 'People') }}
</NcButton>
</template>
</SearchableList>
</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>
{{ provider.name }}
</NcActionButton>
</NcActions>
<NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen">
<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')"
@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" @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">
Expand All @@ -91,8 +101,8 @@
</template>
</NcEmptyContent>
</div>
<div v-for="providerResult in results" :key="providerResult.id" class="unified-search-modal__results">
<div class="results">
<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>
Expand All @@ -115,14 +125,6 @@
</div>
</div>
</div>
<div v-if="supportFiltering()" class="unified-search-modal__results">
<NcButton @click="closeModal">
{{ t('core', 'Filter in current view') }}
<template #icon>
<FilterIcon :size="20" />
</template>
</NcButton>
</div>
</div>
</NcModal>
</template>
Expand All @@ -149,6 +151,7 @@ import SearchResult from '../components/UnifiedSearch/SearchResult.vue'

import debounce from 'debounce'
import { emit } from '@nextcloud/event-bus'
import { useBrowserLocation } from '@vueuse/core'
import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'

export default {
Expand Down Expand Up @@ -179,6 +182,15 @@ export default {
required: true,
},
},
setup() {
/**
* Reactive version of window.location
*/
const currentLocation = useBrowserLocation()
return {
currentLocation,
}
},
data() {
return {
providers: [],
Expand All @@ -205,22 +217,22 @@ export default {
},

computed: {
userContacts: {
get() {
return this.contacts
},
userContacts() {
return this.contacts
},
noContentInfo: {
get() {
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,
}
},
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: {
Expand Down Expand Up @@ -522,21 +534,27 @@ export default {
this.internalIsVisible = false
this.searchQuery = ''
},
supportFiltering() {
/* Hard coded apps for the moment this would be improved in coming updates. */
const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
const currentPath = window.location.pathname.replace('/index.php', '')
const containsProvider = providerPaths.some(path => currentPath.includes(path))
return containsProvider
},
},
}
</script>

<style lang="scss" scoped>
.unified-search-modal {
padding: 10px 20px 10px 20px;
height: 60%;
box-sizing: border-box;
height: 100%;

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;
Expand All @@ -547,14 +565,10 @@ export default {

&__filters {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: start;
padding-top: 4px;
justify-content: left;

>* {
margin-right: 4px;

}

}

&__filters-applied {
Expand All @@ -570,19 +584,19 @@ export default {
}

&__results {
padding: 10px;
overflow: hidden scroll;
padding-block: 0 10px;

.results {

.result-title {
.result {
&-title {
span {
color: var(--color-primary-element);
font-weight: bolder;
font-size: 16px;
}
}

.result-footer {
&-footer {
justify-content: space-between;
align-items: center;
display: flex;
Expand All @@ -592,20 +606,18 @@ export default {
}
}

div.v-popper__wrapper {
ul {
li {
::v-deep button.action-button {
align-items: center !important;

img {
width: 20px;
margin: 0 4px;
filter: var(--background-invert-if-bright);
}
.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>
4 changes: 2 additions & 2 deletions dist/core-common.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/core-common.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/core-unified-search.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/core-unified-search.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@nextcloud/vue": "^8.4.0",
"@skjnldsv/sanitize-svg": "^1.0.2",
"@vueuse/components": "^10.7.2",
"@vueuse/core": "^10.7.2",
"@vueuse/integrations": "^10.7.2",
"autosize": "^6.0.1",
"backbone": "^1.4.1",
Expand Down
Loading