diff options
Diffstat (limited to 'core/src/components')
20 files changed, 359 insertions, 337 deletions
diff --git a/core/src/components/AccountMenu/AccountMenuEntry.vue b/core/src/components/AccountMenu/AccountMenuEntry.vue index c0cff323c12..d983226d273 100644 --- a/core/src/components/AccountMenu/AccountMenuEntry.vue +++ b/core/src/components/AccountMenu/AccountMenuEntry.vue @@ -11,28 +11,30 @@ compact :href="href" :name="name" - target="_self"> + target="_self" + @click="onClick"> <template #icon> - <img class="account-menu-entry__icon" + <NcLoadingIcon v-if="loading" :size="20" class="account-menu-entry__loading" /> + <slot v-else-if="$scopedSlots.icon" name="icon" /> + <img v-else + class="account-menu-entry__icon" :class="{ 'account-menu-entry__icon--active': active }" :src="iconSource" alt=""> </template> - <template v-if="loading" #indicator> - <NcLoadingIcon /> - </template> </NcListItem> </template> -<script> +<script lang="ts"> import { loadState } from '@nextcloud/initial-state' +import { defineComponent } from 'vue' -import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' +import NcListItem from '@nextcloud/vue/components/NcListItem' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' const versionHash = loadState('core', 'versionHash', '') -export default { +export default defineComponent({ name: 'AccountMenuEntry', components: { @@ -55,11 +57,11 @@ export default { }, active: { type: Boolean, - required: true, + default: false, }, icon: { type: String, - required: true, + default: '', }, }, @@ -76,11 +78,17 @@ export default { }, methods: { - handleClick() { - this.loading = true + onClick(e: MouseEvent) { + this.$emit('click', e) + + // Allow to not show the loading indicator + // in case the click event was already handled + if (!e.defaultPrevented) { + this.loading = true + } }, }, -} +}) </script> <style lang="scss" scoped> @@ -96,6 +104,12 @@ export default { } } + &__loading { + height: 20px; + width: 20px; + margin: calc((var(--default-clickable-area) - 20px) / 2); // 20px icon size + } + :deep(.list-item-content__main) { width: fit-content; } diff --git a/core/src/components/AccountMenu/AccountMenuProfileEntry.vue b/core/src/components/AccountMenu/AccountMenuProfileEntry.vue index 853c22986ce..8b895b8ca31 100644 --- a/core/src/components/AccountMenu/AccountMenuProfileEntry.vue +++ b/core/src/components/AccountMenu/AccountMenuProfileEntry.vue @@ -26,8 +26,8 @@ import { getCurrentUser } from '@nextcloud/auth' import { subscribe, unsubscribe } from '@nextcloud/event-bus' import { defineComponent } from 'vue' -import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' +import NcListItem from '@nextcloud/vue/components/NcListItem' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' const { profileEnabled } = loadState('user_status', 'profileEnabled', { profileEnabled: false }) diff --git a/core/src/components/AppMenu.vue b/core/src/components/AppMenu.vue index 265191768af..88f626ff569 100644 --- a/core/src/components/AppMenu.vue +++ b/core/src/components/AppMenu.vue @@ -36,8 +36,8 @@ import { useElementSize } from '@vueuse/core' import { defineComponent, ref } from 'vue' import AppMenuEntry from './AppMenuEntry.vue' -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionLink from '@nextcloud/vue/components/NcActionLink' import logger from '../logger' export default defineComponent({ diff --git a/core/src/components/AppMenuIcon.vue b/core/src/components/AppMenuIcon.vue index f2cee75e644..1b0d48daf8c 100644 --- a/core/src/components/AppMenuIcon.vue +++ b/core/src/components/AppMenuIcon.vue @@ -14,24 +14,25 @@ </template> <script setup lang="ts"> -import type { INavigationEntry } from '../types/navigation' +import type { INavigationEntry } from '../types/navigation.ts' + import { n } from '@nextcloud/l10n' import { computed } from 'vue' - -import IconDot from 'vue-material-design-icons/Circle.vue' +import IconDot from 'vue-material-design-icons/CircleOutline.vue' const props = defineProps<{ app: INavigationEntry }>() -const ariaHidden = computed(() => String(props.app.unread > 0)) +// only hide if there are no unread notifications +const ariaHidden = computed(() => !props.app.unread ? 'true' : undefined) const ariaLabel = computed(() => { - if (ariaHidden.value === 'true') { - return '' + if (!props.app.unread) { + return undefined } - return props.app.name - + (props.app.unread > 0 ? ` (${n('core', '{count} notification', '{count} notifications', props.app.unread, { count: props.app.unread })})` : '') + + return `${props.app.name} (${n('core', '{count} notification', '{count} notifications', props.app.unread, { count: props.app.unread })})` }) </script> @@ -51,6 +52,7 @@ $unread-indicator-size: 10px; height: $icon-size; width: $icon-size; filter: var(--background-image-invert-if-bright); + mask: var(--header-menu-icon-mask); } &__unread { diff --git a/core/src/components/ContactsMenu/Contact.vue b/core/src/components/ContactsMenu/Contact.vue index ec74697341c..322f53647b1 100644 --- a/core/src/components/ContactsMenu/Contact.vue +++ b/core/src/components/ContactsMenu/Contact.vue @@ -54,13 +54,13 @@ </template> <script> -import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' -import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js' -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' -import { getEnabledContactsMenuActions } from '@nextcloud/vue/dist/Functions/contactsMenu.js' +import NcActionLink from '@nextcloud/vue/components/NcActionLink' +import NcActionText from '@nextcloud/vue/components/NcActionText' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import { getEnabledContactsMenuActions } from '@nextcloud/vue/functions/contactsMenu' export default { name: 'Contact', diff --git a/core/src/components/LegacyDialogPrompt.vue b/core/src/components/LegacyDialogPrompt.vue index 5fb21926e4d..f2ee4be9151 100644 --- a/core/src/components/LegacyDialogPrompt.vue +++ b/core/src/components/LegacyDialogPrompt.vue @@ -28,9 +28,9 @@ import { translate as t } from '@nextcloud/l10n' import { defineComponent } from 'vue' -import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' -import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js' +import NcDialog from '@nextcloud/vue/components/NcDialog' +import NcTextField from '@nextcloud/vue/components/NcTextField' +import NcPasswordField from '@nextcloud/vue/components/NcPasswordField' export default defineComponent({ name: 'LegacyDialogPrompt', diff --git a/core/src/components/Profile/PrimaryActionButton.vue b/core/src/components/Profile/PrimaryActionButton.vue index 8ec77e88ea2..dbc446b3d90 100644 --- a/core/src/components/Profile/PrimaryActionButton.vue +++ b/core/src/components/Profile/PrimaryActionButton.vue @@ -21,8 +21,8 @@ <script> import { defineComponent } from 'vue' -import { NcButton } from '@nextcloud/vue' -import { translate as t } from '@nextcloud/l10n' +import { t } from '@nextcloud/l10n' +import NcButton from '@nextcloud/vue/components/NcButton' export default defineComponent({ name: 'PrimaryActionButton', diff --git a/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue index a5a1913ac2b..413806c7089 100644 --- a/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue +++ b/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue @@ -11,22 +11,24 @@ role="presentation" @click="$emit('click')"> <template #icon> - <div role="presentation" :class="['icon', icon, 'public-page-menu-entry__icon']" /> + <slot v-if="$scopedSlots.icon" name="icon" /> + <div v-else role="presentation" :class="['icon', icon, 'public-page-menu-entry__icon']" /> </template> </NcListItem> </template> <script setup lang="ts"> -import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' import { onMounted } from 'vue' +import NcListItem from '@nextcloud/vue/components/NcListItem' + const props = defineProps<{ /** Only emit click event but do not open href */ clickOnly?: boolean // menu entry props id: string label: string - icon: string + icon?: string href: string details?: string }>() diff --git a/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue b/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue index 992ea631600..0f02bdf7524 100644 --- a/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue +++ b/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue @@ -32,10 +32,10 @@ import { generateUrl } from '@nextcloud/router' import { getSharingToken } from '@nextcloud/sharing/public' import { nextTick, onMounted, ref, watch } from 'vue' import axios from '@nextcloud/axios' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcDialog from '@nextcloud/vue/components/NcDialog' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcTextField from '@nextcloud/vue/components/NcTextField' import logger from '../../logger' defineProps<{ diff --git a/core/src/components/UnifiedSearch/CustomDateRangeModal.vue b/core/src/components/UnifiedSearch/CustomDateRangeModal.vue index 332a4286863..d86192d156e 100644 --- a/core/src/components/UnifiedSearch/CustomDateRangeModal.vue +++ b/core/src/components/UnifiedSearch/CustomDateRangeModal.vue @@ -37,9 +37,9 @@ </template> <script> -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcDateTimePicker from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js' -import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePickerNative' +import NcModal from '@nextcloud/vue/components/NcModal' import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue' export default { diff --git a/core/src/components/UnifiedSearch/LegacySearchResult.vue b/core/src/components/UnifiedSearch/LegacySearchResult.vue index 085a6936f2b..4592adf08c9 100644 --- a/core/src/components/UnifiedSearch/LegacySearchResult.vue +++ b/core/src/components/UnifiedSearch/LegacySearchResult.vue @@ -42,7 +42,7 @@ </template> <script> -import NcHighlight from '@nextcloud/vue/dist/Components/NcHighlight.js' +import NcHighlight from '@nextcloud/vue/components/NcHighlight' export default { name: 'LegacySearchResult', diff --git a/core/src/components/UnifiedSearch/SearchResult.vue b/core/src/components/UnifiedSearch/SearchResult.vue index 231ac97642c..4f33fbd54cc 100644 --- a/core/src/components/UnifiedSearch/SearchResult.vue +++ b/core/src/components/UnifiedSearch/SearchResult.vue @@ -3,18 +3,18 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> <template> - <NcListItem class="result-items__item" + <NcListItem class="result-item" :name="title" :bold="false" :href="resourceUrl" target="_self"> <template #icon> <div aria-hidden="true" - class="result-items__item-icon" + class="result-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), + 'result-item__icon--rounded': rounded, + 'result-item__icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl), + 'result-item__icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl), [icon]: !isValidIconOrPreviewUrl(icon), }" :style="{ @@ -32,7 +32,7 @@ </template> <script> -import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' +import NcListItem from '@nextcloud/vue/components/NcListItem' export default { name: 'SearchResult', @@ -101,72 +101,59 @@ export default { </script> <style lang="scss" scoped> -@use "sass:math"; -$clickable-area: 44px; -$margin: 10px; - -.result-items { - &__item:deep { - - a { - 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); - } +.result-item { + :deep(a) { + border: 2px solid transparent; + border-radius: var(--border-radius-large) !important; + + &:active, + &:hover, + &:focus { + background-color: var(--color-background-hover); + border: 2px solid var(--color-border-maxcontrast); + } - * { - cursor: pointer; - } + * { + cursor: pointer; + } + } + &__icon { + overflow: hidden; + width: var(--default-clickable-area); + height: var(--default-clickable-area); + border-radius: var(--border-radius); + background-repeat: no-repeat; + background-position: center center; + background-size: 32px; + + &--rounded { + border-radius: calc(var(--default-clickable-area) / 2); } - &-icon { - overflow: hidden; - width: $clickable-area; - height: $clickable-area; - border-radius: var(--border-radius); - background-repeat: no-repeat; - background-position: center center; + &--no-preview { background-size: 32px; + } - &--rounded { - border-radius: math.div($clickable-area, 2); - } - - &--no-preview { - background-size: 32px; - } - - &--with-thumbnail { - background-size: cover; - } + &--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); - } + &--with-thumbnail:not(#{&}--rounded) { + border: 1px solid var(--color-border); + // compensate for border + max-height: calc(var(--default-clickable-area) - 2px); + max-width: calc(var(--default-clickable-area) - 2px); + } - img { - // Make sure to keep ratio - width: 100%; - height: 100%; + img { + // Make sure to keep ratio + width: 100%; + height: 100%; - object-fit: cover; - object-position: center; - } + object-fit: cover; + object-position: center; } - } } </style> diff --git a/core/src/components/UnifiedSearch/SearchableList.vue b/core/src/components/UnifiedSearch/SearchableList.vue index b2081c2c5ee..d7abb6ffdbb 100644 --- a/core/src/components/UnifiedSearch/SearchableList.vue +++ b/core/src/components/UnifiedSearch/SearchableList.vue @@ -17,7 +17,7 @@ :show-trailing-button="searchTerm !== ''" @update:value="searchTermChanged" @trailing-button-click="clearSearch"> - <Magnify :size="20" /> + <IconMagnify :size="20" /> </NcTextField> <ul v-if="filteredList.length > 0" class="searchable-list__list"> <li v-for="element in filteredList" @@ -42,7 +42,7 @@ <div v-else class="searchable-list__empty-content"> <NcEmptyContent :name="emptyContentText"> <template #icon> - <AlertCircleOutline /> + <IconAlertCircleOutline /> </template> </NcEmptyContent> </div> @@ -51,22 +51,26 @@ </template> <script> -import { NcPopover, NcTextField, NcAvatar, NcEmptyContent, NcButton } from '@nextcloud/vue' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcPopover from '@nextcloud/vue/components/NcPopover' +import NcTextField from '@nextcloud/vue/components/NcTextField' -import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue' -import Magnify from 'vue-material-design-icons/Magnify.vue' +import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue' +import IconMagnify from 'vue-material-design-icons/Magnify.vue' export default { name: 'SearchableList', components: { - NcPopover, - NcTextField, - Magnify, - AlertCircleOutline, + IconMagnify, + IconAlertCircleOutline, NcAvatar, - NcEmptyContent, NcButton, + NcEmptyContent, + NcPopover, + NcTextField, }, props: { diff --git a/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue b/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue index 67853490d9f..171eada8a06 100644 --- a/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue +++ b/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue @@ -32,7 +32,7 @@ {{ t('core', 'Search everywhere') }} </template> <template #icon> - <NcIconSvgWrapper :path="mdiCloudSearch" /> + <NcIconSvgWrapper :path="mdiCloudSearchOutline" /> </template> </NcButton> </div> @@ -41,15 +41,15 @@ <script lang="ts" setup> import type { ComponentPublicInstance } from 'vue' -import { mdiCloudSearch, mdiClose } from '@mdi/js' +import { mdiCloudSearchOutline, mdiClose } from '@mdi/js' import { translate as t } from '@nextcloud/l10n' -import { useIsMobile } from '@nextcloud/vue/dist/Composables/useIsMobile.js' +import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile' +import { useElementSize } from '@vueuse/core' import { computed, ref, watchEffect } from 'vue' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' -import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' -import { useElementSize } from '@vueuse/core' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcInputField from '@nextcloud/vue/components/NcInputField' const props = defineProps<{ query: string, @@ -123,7 +123,7 @@ function clearAndCloseSearch() { // this can break at any time the component library changes :deep(input) { // search global width + close button width - padding-inline-end: calc(v-bind('searchGlobalButtonWidth') + var(--default-clickable-area)); + padding-inline-end: calc(v-bind('searchGlobalButtonCSSWidth') + var(--default-clickable-area)); } } } @@ -132,8 +132,8 @@ function clearAndCloseSearch() { transition: width var(--animation-quick) linear; } -// Make the position absolut during the transition -// this is needed to "hide" the button begind it +// Make the position absolute during the transition +// this is needed to "hide" the button behind it .v-leave-active { position: absolute !important; } @@ -141,7 +141,7 @@ function clearAndCloseSearch() { .v-enter, .v-leave-to { &.local-unified-search { - // Start with only the overlayed button + // Start with only the overlay button --local-search-width: var(--clickable-area-large); } } diff --git a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue index 7400956f96b..b21c65301c4 100644 --- a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue +++ b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue @@ -121,7 +121,7 @@ </h3> <div v-for="providerResult in results" :key="providerResult.id" class="result"> <h4 :id="`unified-search-result-${providerResult.id}`" class="result-title"> - {{ providerResult.provider }} + {{ providerResult.name }} </h4> <ul class="result-items" :aria-labelledby="`unified-search-result-${providerResult.id}`"> <SearchResult v-for="(result, index) in providerResult.results" @@ -129,14 +129,14 @@ v-bind="result" /> </ul> <div class="result-footer"> - <NcButton type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult.id)"> + <NcButton v-if="providerResult.results.length === providerResult.limit" type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult)"> {{ t('core', 'Load more results') }} <template #icon> <IconDotsHorizontal :size="20" /> </template> </NcButton> <NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" type="tertiary-no-background"> - {{ t('core', 'Search in') }} {{ providerResult.provider }} + {{ t('core', 'Search in') }} {{ providerResult.name }} <template #icon> <IconArrowRight :size="20" /> </template> @@ -159,19 +159,19 @@ import debounce from 'debounce' import { unifiedSearchLogger } from '../../logger' import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue' -import IconAccountGroup from 'vue-material-design-icons/AccountGroup.vue' -import IconCalendarRange from 'vue-material-design-icons/CalendarRange.vue' +import IconAccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue' +import IconCalendarRange from 'vue-material-design-icons/CalendarRangeOutline.vue' import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue' import IconFilter from 'vue-material-design-icons/Filter.vue' import IconListBox from 'vue-material-design-icons/ListBox.vue' import IconMagnify from 'vue-material-design-icons/Magnify.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 NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcInputField from '@nextcloud/vue/components/NcInputField' +import NcDialog from '@nextcloud/vue/components/NcDialog' import CustomDateRangeModal from './CustomDateRangeModal.vue' import FilterChip from './SearchFilterChip.vue' @@ -252,11 +252,10 @@ export default defineComponent({ 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: '', + lastSearchQuery: '', placessearchTerm: '', dateTimeFilter: null, filters: [], @@ -264,6 +263,7 @@ export default defineComponent({ contacts: [], showDateRangeModal: false, internalIsVisible: this.open, + initialized: false, } }, @@ -308,6 +308,18 @@ export default defineComponent({ // Load results when opened with already filled query if (this.open) { this.focusInput() + if (!this.initialized) { + Promise.all([getProviders(), getContacts({ searchTerm: '' })]) + .then(([providers, contacts]) => { + this.providers = this.groupProvidersByApp([...providers, ...this.externalFilters]) + this.contacts = this.mapContacts(contacts) + unifiedSearchLogger.debug('Search providers and contacts initialized:', { providers: this.providers, contacts: this.contacts }) + this.initialized = true + }) + .catch((error) => { + unifiedSearchLogger.error(error) + }) + } if (this.searchQuery) { this.find(this.searchQuery) } @@ -317,25 +329,19 @@ export default defineComponent({ query: { immediate: true, handler() { - this.searchQuery = this.query.trim() + this.searchQuery = this.query + }, + }, + + searchQuery: { + handler() { + this.$emit('update:query', this.searchQuery) }, }, }, 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) - unifiedSearchLogger.debug('Search providers', { providers: this.providers }) - }) - getContacts({ searchTerm: '' }).then((contacts) => { - this.contacts = this.mapContacts(contacts) - unifiedSearchLogger.debug('Contacts', { contacts: this.contacts }) - }) }, methods: { /** @@ -361,19 +367,25 @@ export default defineComponent({ this.$refs.searchInput?.focus() }) }, - find(query: string) { + find(query: string, providersToSearchOverride = null) { if (query.length === 0) { this.results = [] this.searching = false return } + // Reset the provider result limit when performing a new search + if (query !== this.lastSearchQuery) { + this.providerResultLimit = 5 + } + this.lastSearchQuery = query + this.searching = true const newResults = [] - const providersToSearch = this.filteredProviders.length > 0 ? this.filteredProviders : this.providers - const searchProvider = (provider, filters) => { + const providersToSearch = providersToSearchOverride || (this.filteredProviders.length > 0 ? this.filteredProviders : this.providers) + const searchProvider = (provider) => { const params = { - type: provider.id, + type: provider.searchFrom ?? provider.id, query, cursor: null, extraQueries: provider.extraParams, @@ -381,31 +393,38 @@ export default defineComponent({ // This block of filter checks should be dynamic somehow and should be handled in // nextcloud/search lib - if (filters.dateFilterIsApplied) { - if (provider.filters?.since && provider.filters?.until) { - params.since = this.dateFilter.startFrom - params.until = this.dateFilter.endAt - } - } + const activeFilters = this.filters.filter(filter => { + return filter.type !== 'provider' && this.providerIsCompatibleWithFilters(provider, [filter.type]) + }) - if (filters.personFilterIsApplied) { - if (provider.filters?.person) { - params.person = this.personFilter.user + activeFilters.forEach(filter => { + switch (filter.type) { + case 'date': + if (provider.filters?.since && provider.filters?.until) { + params.since = this.dateFilter.startFrom + params.until = this.dateFilter.endAt + } + break + case 'person': + if (provider.filters?.person) { + params.person = this.personFilter.user + } + break } - } + }) if (this.providerResultLimit > 5) { params.limit = this.providerResultLimit + unifiedSearchLogger.debug('Limiting search to', params.limit) } const request = unifiedSearch(params).request request().then((response) => { newResults.push({ - id: provider.id, - provider: provider.name, - inAppSearch: provider.inAppSearch, + ...provider, results: response.data.ocs.data.entries, + limit: params.limit ?? 5, }) unifiedSearchLogger.debug('Unified search results:', { results: this.results, newResults }) @@ -414,12 +433,8 @@ export default defineComponent({ this.searching = false }) } - providersToSearch.forEach(provider => { - const dateFilterIsApplied = this.dateFilterIsApplied - const personFilterIsApplied = this.personFilterIsApplied - searchProvider(provider, { dateFilterIsApplied, personFilterIsApplied }) - }) + providersToSearch.forEach(searchProvider) }, updateResults(newResults) { let updatedResults = [...this.results] @@ -477,7 +492,7 @@ export default defineComponent({ }) }, applyPersonFilter(person) { - this.personFilterIsApplied = true + const existingPersonFilter = this.filters.findIndex(filter => filter.id === person.id) if (existingPersonFilter === -1) { this.personFilter.id = person.id @@ -497,16 +512,20 @@ export default defineComponent({ this.debouncedFind(this.searchQuery) unifiedSearchLogger.debug('Person filter applied', { person }) }, - loadMoreResultsForProvider(providerId) { + async loadMoreResultsForProvider(provider) { 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) + this.find(this.searchQuery, [provider]) }, addProviderFilter(providerFilter, loadMoreResultsForProvider = false) { + unifiedSearchLogger.debug('Applying provider filter', { providerFilter, loadMoreResultsForProvider }) if (!providerFilter.id) return if (providerFilter.isPluginFilter) { - providerFilter.callback() + // There is no way to know what should go into the callback currently + // Here we are passing isProviderFilterApplied (boolean) which is a flag sent to the plugin + // This is sent to the plugin so that depending on whether the filter is applied or not, the plugin can decide what to do + // TODO : In nextcloud/search, this should be a proper interface that the plugin can implement + const isProviderFilterApplied = this.filteredProviders.some(provider => provider.id === providerFilter.id) + providerFilter.callback(!isProviderFilterApplied) } this.providerResultLimit = loadMoreResultsForProvider ? this.providerResultLimit : 5 this.providerActionMenuIsOpen = false @@ -519,11 +538,8 @@ export default defineComponent({ this.filters = this.syncProviderFilters(this.filters, this.filteredProviders) } this.filteredProviders.push({ - id: providerFilter.id, - name: providerFilter.name, - icon: providerFilter.icon, + ...providerFilter, type: providerFilter.type || 'provider', - filters: providerFilter.filters, isPluginFilter: providerFilter.isPluginFilter || false, }) this.filters = this.syncProviderFilters(this.filters, this.filteredProviders) @@ -542,14 +558,10 @@ export default defineComponent({ unifiedSearchLogger.debug('Search filters (recently removed)', { filters: this.filters }) } else { + // Remove non provider filters such as date and person filters 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 + if (this.filters[i].id === filter.id) { this.filters.splice(i, 1) - if (filter.type === 'person') { - this.personFilterIsApplied = false - } this.enableAllProviders() break } @@ -588,7 +600,7 @@ export default defineComponent({ } else { this.filters.push(this.dateFilter) } - this.dateFilterIsApplied = true + this.providers.forEach(async (provider, index) => { this.providers[index].disabled = !(await this.providerIsCompatibleWithFilters(provider, ['since', 'until'])) }) @@ -648,6 +660,7 @@ export default defineComponent({ this.updateDateFilter() }, handlePluginFilter(addFilterEvent) { + unifiedSearchLogger.debug('Handling plugin filter', { addFilterEvent }) for (let i = 0; i < this.filteredProviders.length; i++) { const provider = this.filteredProviders[i] if (provider.id === addFilterEvent.id) { diff --git a/core/src/components/login/LoginButton.vue b/core/src/components/login/LoginButton.vue index fcfdb4d01d9..da387df0ff6 100644 --- a/core/src/components/login/LoginButton.vue +++ b/core/src/components/login/LoginButton.vue @@ -20,7 +20,7 @@ <script> import { translate as t } from '@nextcloud/l10n' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcButton from '@nextcloud/vue/components/NcButton' import ArrowRight from 'vue-material-design-icons/ArrowRight.vue' export default { diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue index d031f14140a..8cbe55f1f68 100644 --- a/core/src/components/login/LoginForm.vue +++ b/core/src/components/login/LoginForm.vue @@ -17,9 +17,9 @@ {{ t('core', 'Please contact your administrator.') }} </NcNoteCard> <NcNoteCard v-if="csrfCheckFailed" - :heading="t('core', 'Temporary error')" + :heading="t('core', 'Session error')" type="error"> - {{ t('core', 'Please try again.') }} + {{ t('core', 'It appears your session token has expired, please refresh the page and try again.') }} </NcNoteCard> <NcNoteCard v-if="messages.length > 0"> <div v-for="(message, index) in messages" @@ -103,9 +103,9 @@ import { translate as t } from '@nextcloud/l10n' import { generateUrl, imagePath } from '@nextcloud/router' import debounce from 'debounce' -import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' -import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' +import NcPasswordField from '@nextcloud/vue/components/NcPasswordField' +import NcTextField from '@nextcloud/vue/components/NcTextField' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import AuthMixin from '../../mixins/auth.js' import LoginButton from './LoginButton.vue' @@ -292,6 +292,7 @@ export default { .login-form { text-align: start; font-size: 1rem; + margin: 0; &__fieldset { width: 100%; @@ -304,5 +305,10 @@ export default { text-align: center; overflow-wrap: anywhere; } + + // Only show the error state if the user interacted with the login box + :deep(input:invalid:not(:user-invalid)) { + border-color: var(--color-border-maxcontrast) !important; + } } </style> diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue index 04db5cef05a..bc4d25bf70f 100644 --- a/core/src/components/login/PasswordLessLoginForm.vue +++ b/core/src/components/login/PasswordLessLoginForm.vue @@ -5,59 +5,70 @@ <template> <form v-if="(isHttps || isLocalhost) && supportsWebauthn" ref="loginForm" + aria-labelledby="password-less-login-form-title" + class="password-less-login-form" method="post" name="login" @submit.prevent="submit"> - <h2>{{ t('core', 'Log in with a device') }}</h2> - <fieldset> - <NcTextField required - :value="user" - :autocomplete="autoCompleteAllowed ? 'on' : 'off'" - :error="!validCredentials" - :label="t('core', 'Login or email')" - :placeholder="t('core', 'Login or email')" - :helper-text="!validCredentials ? t('core', 'Your account is not setup for passwordless login.') : ''" - @update:value="changeUsername" /> + <h2 id="password-less-login-form-title"> + {{ t('core', 'Log in with a device') }} + </h2> - <LoginButton v-if="validCredentials" - :loading="loading" - @click="authenticate" /> - </fieldset> + <NcTextField required + :value="user" + :autocomplete="autoCompleteAllowed ? 'on' : 'off'" + :error="!validCredentials" + :label="t('core', 'Login or email')" + :placeholder="t('core', 'Login or email')" + :helper-text="!validCredentials ? t('core', 'Your account is not setup for passwordless login.') : ''" + @update:value="changeUsername" /> + + <LoginButton v-if="validCredentials" + :loading="loading" + @click="authenticate" /> </form> - <div v-else-if="!supportsWebauthn" class="update"> - <InformationIcon size="70" /> - <h2>{{ t('core', 'Browser not supported') }}</h2> - <p class="infogroup"> - {{ t('core', 'Passwordless authentication is not supported in your browser.') }} - </p> - </div> - <div v-else-if="!isHttps && !isLocalhost" class="update"> - <LockOpenIcon size="70" /> - <h2>{{ t('core', 'Your connection is not secure') }}</h2> - <p class="infogroup"> - {{ t('core', 'Passwordless authentication is only available over a secure connection.') }} - </p> - </div> + + <NcEmptyContent v-else-if="!isHttps && !isLocalhost" + :name="t('core', 'Your connection is not secure')" + :description="t('core', 'Passwordless authentication is only available over a secure connection.')"> + <template #icon> + <LockOpenIcon /> + </template> + </NcEmptyContent> + + <NcEmptyContent v-else + :name="t('core', 'Browser not supported')" + :description="t('core', 'Passwordless authentication is not supported in your browser.')"> + <template #icon> + <InformationIcon /> + </template> + </NcEmptyContent> </template> -<script> +<script type="ts"> import { browserSupportsWebAuthn } from '@simplewebauthn/browser' +import { defineComponent } from 'vue' import { + NoValidCredentials, startAuthentication, finishAuthentication, } from '../../services/WebAuthnAuthenticationService.ts' + +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcTextField from '@nextcloud/vue/components/NcTextField' + +import InformationIcon from 'vue-material-design-icons/InformationOutline.vue' import LoginButton from './LoginButton.vue' -import InformationIcon from 'vue-material-design-icons/Information.vue' import LockOpenIcon from 'vue-material-design-icons/LockOpen.vue' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import logger from '../../logger' -export default { +export default defineComponent({ name: 'PasswordLessLoginForm', components: { LoginButton, InformationIcon, LockOpenIcon, + NcEmptyContent, NcTextField, }, props: { @@ -138,21 +149,14 @@ export default { // noop }, }, -} +}) </script> <style lang="scss" scoped> - fieldset { - display: flex; - flex-direction: column; - gap: 0.5rem; - - :deep(label) { - text-align: initial; - } - } - - .update { - margin: 0 auto; - } +.password-less-login-form { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 0; +} </style> diff --git a/core/src/components/login/ResetPassword.vue b/core/src/components/login/ResetPassword.vue index 254ad4d8e16..fee1deacc36 100644 --- a/core/src/components/login/ResetPassword.vue +++ b/core/src/components/login/ResetPassword.vue @@ -4,59 +4,65 @@ --> <template> - <form class="login-form" @submit.prevent="submit"> - <fieldset class="login-form__fieldset"> - <NcTextField id="user" - :value.sync="user" - name="user" - :maxlength="255" - autocapitalize="off" - :label="t('core', 'Login or email')" - :error="userNameInputLengthIs255" - :helper-text="userInputHelperText" - required - @change="updateUsername" /> - <LoginButton :value="t('core', 'Reset password')" /> - - <NcNoteCard v-if="message === 'send-success'" - type="success"> - {{ t('core', 'If this account exists, a password reset message has been sent to its email address. If you do not receive it, verify your email address and/or Login, check your spam/junk folders or ask your local administration for help.') }} - </NcNoteCard> - <NcNoteCard v-else-if="message === 'send-error'" - type="error"> - {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }} - </NcNoteCard> - <NcNoteCard v-else-if="message === 'reset-error'" - type="error"> - {{ t('core', 'Password cannot be changed. Please contact your administrator.') }} - </NcNoteCard> - - <a class="login-form__link" - href="#" - @click.prevent="$emit('abort')"> - {{ t('core', 'Back to login') }} - </a> - </fieldset> + <form class="reset-password-form" @submit.prevent="submit"> + <h2>{{ t('core', 'Reset password') }}</h2> + + <NcTextField id="user" + :value.sync="user" + name="user" + :maxlength="255" + autocapitalize="off" + :label="t('core', 'Login or email')" + :error="userNameInputLengthIs255" + :helper-text="userInputHelperText" + required + @change="updateUsername" /> + + <LoginButton :loading="loading" :value="t('core', 'Reset password')" /> + + <NcButton type="tertiary" wide @click="$emit('abort')"> + {{ t('core', 'Back to login') }} + </NcButton> + + <NcNoteCard v-if="message === 'send-success'" + type="success"> + {{ t('core', 'If this account exists, a password reset message has been sent to its email address. If you do not receive it, verify your email address and/or Login, check your spam/junk folders or ask your local administration for help.') }} + </NcNoteCard> + <NcNoteCard v-else-if="message === 'send-error'" + type="error"> + {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }} + </NcNoteCard> + <NcNoteCard v-else-if="message === 'reset-error'" + type="error"> + {{ t('core', 'Password cannot be changed. Please contact your administrator.') }} + </NcNoteCard> </form> </template> -<script> -import axios from '@nextcloud/axios' +<script lang="ts"> import { generateUrl } from '@nextcloud/router' -import LoginButton from './LoginButton.vue' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' -import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' +import { defineComponent } from 'vue' + +import axios from '@nextcloud/axios' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcTextField from '@nextcloud/vue/components/NcTextField' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import AuthMixin from '../../mixins/auth.js' +import LoginButton from './LoginButton.vue' +import logger from '../../logger.js' -export default { +export default defineComponent({ name: 'ResetPassword', components: { LoginButton, + NcButton, NcNoteCard, NcTextField, }, + mixins: [AuthMixin], + props: { username: { type: String, @@ -67,11 +73,12 @@ export default { required: true, }, }, + data() { return { error: false, loading: false, - message: undefined, + message: '', user: this.username, } }, @@ -84,56 +91,38 @@ export default { updateUsername() { this.$emit('update:username', this.user) }, - submit() { + + async submit() { this.loading = true this.error = false this.message = '' const url = generateUrl('/lostpassword/email') - const data = { - user: this.user, - } + try { + const { data } = await axios.post(url, { user: this.user }) + if (data.status !== 'success') { + throw new Error(`got status ${data.status}`) + } + + this.message = 'send-success' + } catch (error) { + logger.error('could not send reset email request', { error }) - return axios.post(url, data) - .then(resp => resp.data) - .then(data => { - if (data.status !== 'success') { - throw new Error(`got status ${data.status}`) - } - - this.message = 'send-success' - }) - .catch(e => { - console.error('could not send reset email request', e) - - this.error = true - this.message = 'send-error' - }) - .then(() => { this.loading = false }) + this.error = true + this.message = 'send-error' + } finally { + this.loading = false + } }, }, -} +}) </script> <style lang="scss" scoped> -.login-form { - text-align: start; - font-size: 1rem; - - &__fieldset { - width: 100%; - display: flex; - flex-direction: column; - gap: .5rem; - } - - &__link { - display: block; - font-weight: normal !important; - cursor: pointer; - font-size: var(--default-font-size); - text-align: center; - padding: .5rem 1rem 1rem 1rem; - } +.reset-password-form { + display: flex; + flex-direction: column; + gap: .5rem; + width: 100%; } </style> diff --git a/core/src/components/setup/RecommendedApps.vue b/core/src/components/setup/RecommendedApps.vue index d6968bb53e4..f2120c28402 100644 --- a/core/src/components/setup/RecommendedApps.vue +++ b/core/src/components/setup/RecommendedApps.vue @@ -4,7 +4,7 @@ --> <template> - <div class="guest-box"> + <div class="guest-box" data-cy-setup-recommended-apps> <h2>{{ t('core', 'Recommended apps') }}</h2> <p v-if="loadingApps" class="loading text-center"> {{ t('core', 'Loading apps …') }} @@ -38,15 +38,16 @@ <div class="dialog-row"> <NcButton v-if="showInstallButton && !installingApps" - type="tertiary" - role="link" - :href="defaultPageUrl"> + data-cy-setup-recommended-apps-skip + :href="defaultPageUrl" + variant="tertiary"> {{ t('core', 'Skip') }} </NcButton> <NcButton v-if="showInstallButton" - type="primary" + data-cy-setup-recommended-apps-install :disabled="installingApps || !isAnyAppSelected" + variant="primary" @click.stop.prevent="installApps"> {{ installingApps ? t('core', 'Installing apps …') : t('core', 'Install recommended apps') }} </NcButton> @@ -62,8 +63,8 @@ import axios from '@nextcloud/axios' import pLimit from 'p-limit' import logger from '../../logger.js' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' const recommended = { calendar: { |