aboutsummaryrefslogtreecommitdiffstats
path: root/core/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/components')
-rw-r--r--core/src/components/AccountMenu/AccountMenuEntry.vue42
-rw-r--r--core/src/components/AccountMenu/AccountMenuProfileEntry.vue4
-rw-r--r--core/src/components/AppMenu.vue4
-rw-r--r--core/src/components/AppMenuIcon.vue18
-rw-r--r--core/src/components/ContactsMenu/Contact.vue14
-rw-r--r--core/src/components/LegacyDialogPrompt.vue6
-rw-r--r--core/src/components/Profile/PrimaryActionButton.vue4
-rw-r--r--core/src/components/PublicPageMenu/PublicPageMenuEntry.vue8
-rw-r--r--core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue8
-rw-r--r--core/src/components/UnifiedSearch/CustomDateRangeModal.vue6
-rw-r--r--core/src/components/UnifiedSearch/LegacySearchResult.vue2
-rw-r--r--core/src/components/UnifiedSearch/SearchResult.vue111
-rw-r--r--core/src/components/UnifiedSearch/SearchableList.vue24
-rw-r--r--core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue22
-rw-r--r--core/src/components/UnifiedSearch/UnifiedSearchModal.vue145
-rw-r--r--core/src/components/login/LoginButton.vue2
-rw-r--r--core/src/components/login/LoginForm.vue16
-rw-r--r--core/src/components/login/PasswordLessLoginForm.vue96
-rw-r--r--core/src/components/login/ResetPassword.vue149
-rw-r--r--core/src/components/setup/RecommendedApps.vue15
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: {