Move search result section to component & hide thumbnail if error

The section that handles the search result list item is big and complex enough
 to have it's own component. Inside this component, a new method is added to hide
the thumbnail preview if the image load errors out.

Signed-off-by: fenn-cs <fenn25.fn@gmail.com>
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
pull/41507/head
fenn-cs 2023-11-18 13:40:04 +07:00 committed by nextcloud-command
parent 8dadb09e54
commit 701e626d18
5 changed files with 175 additions and 102 deletions

@ -5,7 +5,7 @@
:show.sync="isModalOpen"
:size="'small'"
:clear-view-delay="0"
:title="t('Custom date range')"
:title="t('core', 'Custom date range')"
@close="closeModal">
<!-- Custom date range -->
<div class="global-search-custom-date-modal">

@ -0,0 +1,168 @@
<template>
<NcListItem class="result-items__item"
:name="title"
:bold="false"
@click="openResult(result)">
<template #icon>
<div aria-hidden="true"
class="result-items__item-icon"
:class="{
'result-items__item-icon--rounded': rounded,
'result-items__item-icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl),
'result-items__item-icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl),
[icon]: !isValidIconOrPreviewUrl(icon),
}"
:style="{
backgroundImage: isValidIconOrPreviewUrl(icon) ? `url(${icon})` : '',
}">
<img v-if="isValidIconOrPreviewUrl(thumbnailUrl) && !thumbnailHasError"
:src="thumbnailUrl"
@error="thumbnailErrorHandler">
</div>
</template>
<template #subname>
{{ subline }}
</template>
</NcListItem>
</template>
<script>
import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
export default {
name: 'SearchResult',
components: {
NcListItem,
},
props: {
thumbnailUrl: {
type: String,
default: null,
},
title: {
type: String,
required: true,
},
subline: {
type: String,
default: null,
},
resourceUrl: {
type: String,
default: null,
},
icon: {
type: String,
default: '',
},
rounded: {
type: Boolean,
default: false,
},
query: {
type: String,
default: '',
},
/**
* Only used for the first result as a visual feedback
* so we can keep the search input focused but pressing
* enter still opens the first result
*/
focused: {
type: Boolean,
default: false,
},
},
data() {
return {
thumbnailHasError: false,
}
},
watch: {
thumbnailUrl() {
this.thumbnailHasError = false
},
},
methods: {
isValidIconOrPreviewUrl(url) {
return /^https?:\/\//.test(url) || url.startsWith('/')
},
thumbnailErrorHandler() {
this.thumbnailHasError = true
},
},
}
</script>
<style lang="scss" scoped>
@use "sass:math";
$clickable-area: 44px;
$margin: 10px;
.result-items {
&__item {
::v-deep a {
border-radius: 12px;
border: 2px solid transparent;
border-radius: var(--border-radius-large) !important;
&--focused {
background-color: var(--color-background-hover);
}
&:active,
&:hover,
&:focus {
background-color: var(--color-background-hover);
border: 2px solid var(--color-border-maxcontrast);
}
* {
cursor: pointer;
}
}
&-icon {
overflow: hidden;
width: $clickable-area;
height: $clickable-area;
border-radius: var(--border-radius);
background-repeat: no-repeat;
background-position: center center;
background-size: 32px;
&--rounded {
border-radius: math.div($clickable-area, 2);
}
&--no-preview {
background-size: 32px;
}
&--with-thumbnail {
background-size: cover;
}
&--with-thumbnail:not(&--rounded) {
// compensate for border
max-width: $clickable-area - 2px;
max-height: $clickable-area - 2px;
border: 1px solid var(--color-border);
}
img {
// Make sure to keep ratio
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
}
}
}
</style>

@ -97,30 +97,7 @@
<span>{{ providerResult.provider }}</span>
</div>
<ul class="result-items">
<NcListItem v-for="(result, index) in providerResult.results"
:key="index"
class="result-items__item"
:name="result.title ?? ''"
:bold="false"
@click="openResult(result)">
<template #icon>
<div class="result-items__item-icon"
:class="{
'result-items__item-icon--rounded': result.rounded,
'result-items__item-icon--no-preview': !isValidIconOrPreviewUrl(result.thumbnailUrl),
'result-items__item-icon--with-thumbnail': isValidIconOrPreviewUrl(result.thumbnailUrl),
[result.icon]: !isValidIconOrPreviewUrl(result.icon),
}"
:style="{
backgroundImage: isValidIconOrPreviewUrl(result.icon) ? `url(${result.icon})` : '',
}">
<img v-if="isValidIconOrPreviewUrl(result.thumbnailUrl)" :src="result.thumbnailUrl" class="">
</div>
</template>
<template #subname>
{{ result.subline }}
</template>
</NcListItem>
<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)">
@ -158,9 +135,9 @@ 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 NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
import MagnifyIcon from 'vue-material-design-icons/Magnify.vue'
import SearchableList from '../components/GlobalSearch/SearchableList.vue'
import SearchResult from '../components/GlobalSearch/SearchResult.vue'
import debounce from 'debounce'
import { getProviders, search as globalSearch, getContacts } from '../services/GlobalSearchService.js'
@ -182,10 +159,10 @@ export default {
NcButton,
NcEmptyContent,
NcModal,
NcListItem,
NcInputField,
MagnifyIcon,
SearchableList,
SearchResult,
},
props: {
isVisible: {
@ -512,9 +489,6 @@ export default {
this.dateFilter.text = t('core', `Between ${this.dateFilter.startFrom.toLocaleDateString()} and ${this.dateFilter.endAt.toLocaleDateString()}`)
this.updateDateFilter()
},
isValidIconOrPreviewUrl(url) {
return /^https?:\/\//.test(url) || url.startsWith('/')
},
closeModal() {
this.searchQuery = ''
},
@ -523,10 +497,6 @@ export default {
</script>
<style lang="scss" scoped>
@use "sass:math";
$clickable-area: 44px;
$margin: 10px;
.global-search-modal {
padding: 10px 20px 10px 20px;
height: 60%;
@ -574,71 +544,6 @@ $margin: 10px;
}
}
.result-items {
::v-deep &__item {
a {
border-radius: 12px;
border: 2px solid transparent;
border-radius: var(--border-radius-large) !important;
&--focused {
background-color: var(--color-background-hover);
}
&:active,
&:hover,
&:focus {
background-color: var(--color-background-hover);
border: 2px solid var(--color-border-maxcontrast);
}
* {
cursor: pointer;
}
}
&-icon {
overflow: hidden;
width: $clickable-area;
height: $clickable-area;
border-radius: var(--border-radius);
background-repeat: no-repeat;
background-position: center center;
background-size: 32px;
&--rounded {
border-radius: math.div($clickable-area, 2);
}
&--no-preview {
background-size: 32px;
}
&--with-thumbnail {
background-size: cover;
}
&--with-thumbnail:not(&--rounded) {
// compensate for border
max-width: $clickable-area - 2px;
max-height: $clickable-area - 2px;
border: 1px solid var(--color-border);
}
img {
// Make sure to keep ratio
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
}
}
}
.result-footer {
justify-content: space-between;
align-items: center;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long