aboutsummaryrefslogtreecommitdiffstats
path: root/core/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/views')
-rw-r--r--core/src/views/AccountMenu.vue50
-rw-r--r--core/src/views/ContactsMenu.vue37
-rw-r--r--core/src/views/LegacyUnifiedSearch.vue12
-rw-r--r--core/src/views/Login.vue102
-rw-r--r--core/src/views/PublicPageMenu.vue8
-rw-r--r--core/src/views/PublicPageUserMenu.vue138
-rw-r--r--core/src/views/Setup.cy.ts369
-rw-r--r--core/src/views/Setup.vue460
-rw-r--r--core/src/views/UnifiedSearch.vue61
-rw-r--r--core/src/views/UnsupportedBrowser.vue4
10 files changed, 1085 insertions, 156 deletions
diff --git a/core/src/views/AccountMenu.vue b/core/src/views/AccountMenu.vue
index 0eb6a76e4dd..5b7ead636bd 100644
--- a/core/src/views/AccountMenu.vue
+++ b/core/src/views/AccountMenu.vue
@@ -47,8 +47,8 @@ import { getAllStatusOptions } from '../../../apps/user_status/src/services/stat
import axios from '@nextcloud/axios'
import logger from '../logger.js'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
import AccountMenuProfileEntry from '../components/AccountMenu/AccountMenuProfileEntry.vue'
import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue'
@@ -197,27 +197,15 @@ export default defineComponent({
}
.account-menu {
- :deep(button) {
- // Normally header menus are slightly translucent when not active
- // this is generally ok but for the avatar this is weird so fix the opacity
- opacity: 1 !important;
-
- // The avatar is just the "icon" of the button
- // So we add the focus-visible manually
- &:focus-visible {
- .account-menu__avatar {
- border: var(--border-width-input-focused) solid var(--color-background-plain-text);
- }
- }
- }
-
- // Ensure we do not wast space, as the header menu sets a default width of 350px
- :deep(.header-menu__content) {
- width: fit-content !important;
- }
-
&__avatar {
+ --account-menu-outline: var(--border-width-input) solid color-mix(in srgb, var(--color-background-plain-text), transparent 75%);
+ outline: var(--account-menu-outline);
+ position: fixed;
+ // do not apply the alpha mask on the avatar div
+ mask: none !important;
+
&:hover {
+ --account-menu-outline: none;
// Add hover styles similar to the focus-visible style
border: var(--border-width-input-focused) solid var(--color-background-plain-text);
}
@@ -235,5 +223,25 @@ export default defineComponent({
flex: 0 1;
}
}
+
+ // Ensure we do not waste space, as the header menu sets a default width of 350px
+ :deep(.header-menu__content) {
+ width: fit-content !important;
+ }
+
+ :deep(button) {
+ // Normally header menus are slightly translucent when not active
+ // this is generally ok but for the avatar this is weird so fix the opacity
+ opacity: 1 !important;
+
+ // The avatar is just the "icon" of the button
+ // So we add the focus-visible manually
+ &:focus-visible {
+ .account-menu__avatar {
+ --account-menu-outline: none;
+ border: var(--border-width-input-focused) solid var(--color-background-plain-text);
+ }
+ }
+ }
}
</style>
diff --git a/core/src/views/ContactsMenu.vue b/core/src/views/ContactsMenu.vue
index b1f8a96f730..924ddcea56b 100644
--- a/core/src/views/ContactsMenu.vue
+++ b/core/src/views/ContactsMenu.vue
@@ -9,7 +9,7 @@
:aria-label="t('core', 'Search contacts')"
@open="handleOpen">
<template #trigger>
- <Contacts class="contactsmenu__trigger-icon" :size="20" />
+ <NcIconSvgWrapper class="contactsmenu__trigger-icon" :path="mdiContacts" />
</template>
<div class="contactsmenu__menu">
<div class="contactsmenu__menu__input-wrapper">
@@ -27,7 +27,7 @@
</div>
<NcEmptyContent v-if="error" :name="t('core', 'Could not load your contacts')">
<template #icon>
- <Magnify />
+ <NcIconSvgWrapper :path="mdiMagnify" />
</template>
</NcEmptyContent>
<NcEmptyContent v-else-if="loadingText" :name="loadingText">
@@ -37,7 +37,7 @@
</NcEmptyContent>
<NcEmptyContent v-else-if="contacts.length === 0" :name="t('core', 'No contacts found')">
<template #icon>
- <Magnify />
+ <NcIconSvgWrapper :path="mdiMagnify" />
</template>
</NcEmptyContent>
<div v-else class="contactsmenu__menu__content">
@@ -62,39 +62,46 @@
</template>
<script>
+import { mdiContacts, mdiMagnify } from '@mdi/js'
+import { generateUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import { t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
-import Contacts from 'vue-material-design-icons/Contacts.vue'
import debounce from 'debounce'
-import { getCurrentUser } from '@nextcloud/auth'
-import { generateUrl } from '@nextcloud/router'
-import Magnify from 'vue-material-design-icons/Magnify.vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import { translate as t } from '@nextcloud/l10n'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import Contact from '../components/ContactsMenu/Contact.vue'
import logger from '../logger.js'
import Nextcloud from '../mixins/Nextcloud.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
export default {
name: 'ContactsMenu',
components: {
Contact,
- Contacts,
- Magnify,
NcButton,
NcEmptyContent,
NcHeaderMenu,
+ NcIconSvgWrapper,
NcLoadingIcon,
NcTextField,
},
mixins: [Nextcloud],
+ setup() {
+ return {
+ mdiContacts,
+ mdiMagnify,
+ }
+ },
+
data() {
const user = getCurrentUser()
return {
diff --git a/core/src/views/LegacyUnifiedSearch.vue b/core/src/views/LegacyUnifiedSearch.vue
index 0bb55dc53e4..1277970ba0e 100644
--- a/core/src/views/LegacyUnifiedSearch.vue
+++ b/core/src/views/LegacyUnifiedSearch.vue
@@ -108,11 +108,11 @@ import debounce from 'debounce'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { showError } from '@nextcloud/dialogs'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import Magnify from 'vue-material-design-icons/Magnify.vue'
@@ -270,7 +270,7 @@ export default {
return n('core',
'Please enter {minSearchLength} character or more to search',
- 'Please enter {minSearchLength} characters or more to search',
+ 'Please enter {minSearchLength} characters or more to search',
this.minSearchLength,
{ minSearchLength: this.minSearchLength })
},
diff --git a/core/src/views/Login.vue b/core/src/views/Login.vue
index a13109bb766..a6fe8442779 100644
--- a/core/src/views/Login.vue
+++ b/core/src/views/Login.vue
@@ -7,7 +7,7 @@
<div class="guest-box login-box">
<template v-if="!hideLoginForm || directLogin">
<transition name="fade" mode="out-in">
- <div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''">
+ <div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''" class="login-box__wrapper">
<LoginForm :username.sync="user"
:redirect-url="redirectUrl"
:direct-login="directLogin"
@@ -17,40 +17,30 @@
:auto-complete-allowed="autoCompleteAllowed"
:email-states="emailStates"
@submit="loading = true" />
- <a v-if="canResetPassword && resetPasswordLink !== ''"
+ <NcButton v-if="hasPasswordless"
+ type="tertiary"
+ wide
+ @click.prevent="passwordlessLogin = true">
+ {{ t('core', 'Log in with a device') }}
+ </NcButton>
+ <NcButton v-if="canResetPassword && resetPasswordLink !== ''"
id="lost-password"
- class="login-box__link"
- :href="resetPasswordLink">
+ :href="resetPasswordLink"
+ type="tertiary-no-background"
+ wide>
{{ t('core', 'Forgot password?') }}
- </a>
- <a v-else-if="canResetPassword && !resetPassword"
+ </NcButton>
+ <NcButton v-else-if="canResetPassword && !resetPassword"
id="lost-password"
- class="login-box__link"
- :href="resetPasswordLink"
+ type="tertiary"
+ wide
@click.prevent="resetPassword = true">
{{ t('core', 'Forgot password?') }}
- </a>
- <template v-if="hasPasswordless">
- <div v-if="countAlternativeLogins"
- class="alternative-logins">
- <a v-if="hasPasswordless"
- class="button"
- :class="{ 'single-alt-login-option': countAlternativeLogins }"
- href="#"
- @click.prevent="passwordlessLogin = true">
- {{ t('core', 'Log in with a device') }}
- </a>
- </div>
- <a v-else
- href="#"
- @click.prevent="passwordlessLogin = true">
- {{ t('core', 'Log in with a device') }}
- </a>
- </template>
+ </NcButton>
</div>
<div v-else-if="!loading && passwordlessLogin"
key="reset-pw-less"
- class="login-additional login-passwordless">
+ class="login-additional login-box__wrapper">
<PasswordLessLoginForm :username.sync="user"
:redirect-url="redirectUrl"
:auto-complete-allowed="autoCompleteAllowed"
@@ -89,7 +79,7 @@
</transition>
</template>
- <div id="alternative-logins" class="alternative-logins">
+ <div id="alternative-logins" class="login-box__alternative-logins">
<NcButton v-for="(alternativeLogin, index) in alternativeLogins"
:key="index"
type="secondary"
@@ -105,24 +95,21 @@
<script>
import { loadState } from '@nextcloud/initial-state'
+import { generateUrl } from '@nextcloud/router'
+
import queryString from 'query-string'
import LoginForm from '../components/login/LoginForm.vue'
import PasswordLessLoginForm from '../components/login/PasswordLessLoginForm.vue'
import ResetPassword from '../components/login/ResetPassword.vue'
import UpdatePassword from '../components/login/UpdatePassword.vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import { wipeBrowserStorages } from '../utils/xhr-request.js'
const query = queryString.parse(location.search)
if (query.clear === '1') {
- try {
- window.localStorage.clear()
- window.sessionStorage.clear()
- console.debug('Browser storage cleared')
- } catch (e) {
- console.error('Could not clear browser storage', e)
- }
+ wipeBrowserStorages()
}
export default {
@@ -167,29 +154,28 @@ export default {
methods: {
passwordResetFinished() {
- this.resetPasswordTarget = ''
- this.directLogin = true
+ window.location.href = generateUrl('login')
},
},
}
</script>
-<style lang="scss">
-body {
- font-size: var(--default-font-size);
-}
-
+<style scoped lang="scss">
.login-box {
// Same size as dashboard panels
width: 320px;
box-sizing: border-box;
- &__link {
- display: block;
- padding: 1rem;
- font-size: var(--default-font-size);
- text-align: center;
- font-weight: normal !important;
+ &__wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: calc(2 * var(--default-grid-baseline));
+ }
+
+ &__alternative-logins {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
}
}
@@ -200,20 +186,4 @@ body {
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
-
-.alternative-logins {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-
- .button-vue {
- box-sizing: border-box;
- }
-}
-
-.login-passwordless {
- .button-vue {
- margin-top: 0.5rem;
- }
-}
</style>
diff --git a/core/src/views/PublicPageMenu.vue b/core/src/views/PublicPageMenu.vue
index a9ff78a7c5f..a05f3a6b889 100644
--- a/core/src/views/PublicPageMenu.vue
+++ b/core/src/views/PublicPageMenu.vue
@@ -37,13 +37,13 @@
</template>
<script setup lang="ts">
-import { spawnDialog } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
-import { useIsSmallMobile } from '@nextcloud/vue/dist/Composables/useIsMobile.js'
+import { useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
+import { spawnDialog } from '@nextcloud/vue/functions/dialog'
import { computed, ref, type Ref } from 'vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
import IconMore from 'vue-material-design-icons/DotsHorizontal.vue'
import PublicPageMenuEntry from '../components/PublicPageMenu/PublicPageMenuEntry.vue'
import PublicPageMenuCustomEntry from '../components/PublicPageMenu/PublicPageMenuCustomEntry.vue'
diff --git a/core/src/views/PublicPageUserMenu.vue b/core/src/views/PublicPageUserMenu.vue
new file mode 100644
index 00000000000..7bd6521e7aa
--- /dev/null
+++ b/core/src/views/PublicPageUserMenu.vue
@@ -0,0 +1,138 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<template>
+ <NcHeaderMenu id="public-page-user-menu"
+ class="public-page-user-menu"
+ is-nav
+ :aria-label="t('core', 'User menu')"
+ :description="avatarDescription">
+ <template #trigger>
+ <NcAvatar class="public-page-user-menu__avatar"
+ disable-menu
+ disable-tooltip
+ is-guest
+ :user="displayName || '?'" />
+ </template>
+
+ <!-- Privacy notice -->
+ <NcNoteCard class="public-page-user-menu__list-note"
+ :text="privacyNotice"
+ type="info" />
+
+ <ul class="public-page-user-menu__list">
+ <!-- Nickname dialog -->
+ <AccountMenuEntry id="set-nickname"
+ :name="!displayName ? t('core', 'Set public name') : t('core', 'Change public name')"
+ href="#"
+ @click.prevent.stop="setNickname">
+ <template #icon>
+ <IconAccount />
+ </template>
+ </AccountMenuEntry>
+ </ul>
+ </NcHeaderMenu>
+</template>
+
+<script lang="ts">
+import type { NextcloudUser } from '@nextcloud/auth'
+
+import '@nextcloud/dialogs/style.css'
+import { defineComponent } from 'vue'
+import { getGuestUser } from '@nextcloud/auth'
+import { showGuestUserPrompt } from '@nextcloud/dialogs'
+import { subscribe } from '@nextcloud/event-bus'
+import { t } from '@nextcloud/l10n'
+
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import IconAccount from 'vue-material-design-icons/AccountOutline.vue'
+
+import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue'
+
+export default defineComponent({
+ name: 'PublicPageUserMenu',
+ components: {
+ AccountMenuEntry,
+ IconAccount,
+ NcAvatar,
+ NcHeaderMenu,
+ NcNoteCard,
+ },
+
+ setup() {
+ return {
+ t,
+ }
+ },
+
+ data() {
+ return {
+ displayName: getGuestUser().displayName,
+ }
+ },
+
+ computed: {
+ avatarDescription(): string {
+ return t('core', 'User menu')
+ },
+
+ privacyNotice(): string {
+ return this.displayName
+ ? t('core', 'You will be identified as {user} by the account owner.', { user: this.displayName })
+ : t('core', 'You are currently not identified.')
+ },
+ },
+
+ mounted() {
+ subscribe('user:info:changed', (user: NextcloudUser) => {
+ this.displayName = user.displayName || ''
+ })
+ },
+
+ methods: {
+ setNickname() {
+ showGuestUserPrompt({
+ nickname: this.displayName,
+ cancellable: true,
+ })
+ },
+ },
+})
+</script>
+
+<style scoped lang="scss">
+.public-page-user-menu {
+ &, * {
+ box-sizing: border-box;
+ }
+
+ // Ensure we do not waste space, as the header menu sets a default width of 350px
+ :deep(.header-menu__content) {
+ width: fit-content !important;
+ }
+
+ &__list-note {
+ padding-block: 5px !important;
+ padding-inline: 5px !important;
+ max-width: 300px;
+ margin: 5px !important;
+ margin-bottom: 0 !important;
+ }
+
+ &__list {
+ display: inline-flex;
+ flex-direction: column;
+ padding-block: var(--default-grid-baseline) 0;
+ width: 100%;
+
+ > :deep(li) {
+ box-sizing: border-box;
+ // basically "fit-content"
+ flex: 0 1;
+ }
+ }
+}
+</style>
diff --git a/core/src/views/Setup.cy.ts b/core/src/views/Setup.cy.ts
new file mode 100644
index 00000000000..f252801c4d8
--- /dev/null
+++ b/core/src/views/Setup.cy.ts
@@ -0,0 +1,369 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { SetupConfig, SetupLinks } from '../install'
+import SetupView from './Setup.vue'
+
+import '../../css/guest.css'
+
+const defaultConfig = Object.freeze({
+ adminlogin: '',
+ adminpass: '',
+ dbuser: '',
+ dbpass: '',
+ dbname: '',
+ dbtablespace: '',
+ dbhost: '',
+ dbtype: '',
+ databases: {
+ sqlite: 'SQLite',
+ mysql: 'MySQL/MariaDB',
+ pgsql: 'PostgreSQL',
+ },
+ directory: '',
+ hasAutoconfig: false,
+ htaccessWorking: true,
+ serverRoot: '/var/www/html',
+ errors: [],
+}) as SetupConfig
+
+const links = {
+ adminInstall: 'https://docs.nextcloud.com/server/32/go.php?to=admin-install',
+ adminSourceInstall: 'https://docs.nextcloud.com/server/32/go.php?to=admin-source_install',
+ adminDBConfiguration: 'https://docs.nextcloud.com/server/32/go.php?to=admin-db-configuration',
+} as SetupLinks
+
+describe('Default setup page', () => {
+ beforeEach(() => {
+ cy.mockInitialState('core', 'links', links)
+ })
+
+ afterEach(() => cy.unmockInitialState())
+
+ it('Renders default config', () => {
+ cy.mockInitialState('core', 'config', defaultConfig)
+ cy.mount(SetupView)
+
+ cy.get('[data-cy-setup-form]').scrollIntoView()
+ cy.get('[data-cy-setup-form]').should('be.visible')
+
+ // Single note is the footer help
+ cy.get('[data-cy-setup-form-note]')
+ .should('have.length', 1)
+ .should('be.visible')
+ cy.get('[data-cy-setup-form-note]').should('contain', 'See the documentation')
+
+ // DB radio selectors
+ cy.get('[data-cy-setup-form-field^="dbtype"]')
+ .should('exist')
+ .find('input')
+ .should('be.checked')
+
+ cy.get('[data-cy-setup-form-field="dbtype-mysql"]').should('exist')
+ cy.get('[data-cy-setup-form-field="dbtype-pgsql"]').should('exist')
+ cy.get('[data-cy-setup-form-field="dbtype-oci"]').should('not.exist')
+
+ // Sqlite warning
+ cy.get('[data-cy-setup-form-db-note="sqlite"]')
+ .should('be.visible')
+
+ // admin login, password, data directory and 3 DB radio selectors
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 6)
+ })
+
+ it('Renders single DB sqlite', () => {
+ const config = {
+ ...defaultConfig,
+ databases: {
+ sqlite: 'SQLite',
+ },
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // No DB radio selectors if only sqlite
+ cy.get('[data-cy-setup-form-field^="dbtype"]')
+ .should('not.exist')
+
+ // Two warnings: sqlite and single db support
+ cy.get('[data-cy-setup-form-db-note="sqlite"]')
+ .should('be.visible')
+ cy.get('[data-cy-setup-form-db-note="single-db"]')
+ .should('be.visible')
+
+ // Admin login, password and data directory
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 3)
+ })
+
+ it('Renders single DB mysql', () => {
+ const config = {
+ ...defaultConfig,
+ databases: {
+ mysql: 'MySQL/MariaDB',
+ },
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // No DB radio selectors if only mysql
+ cy.get('[data-cy-setup-form-field^="dbtype"]')
+ .should('not.exist')
+
+ // Single db support warning
+ cy.get('[data-cy-setup-form-db-note="single-db"]')
+ .should('be.visible')
+ .invoke('html')
+ .should('contains', links.adminSourceInstall)
+
+ // No SQLite warning
+ cy.get('[data-cy-setup-form-db-note="sqlite"]')
+ .should('not.exist')
+
+ // Admin login, password, data directory, db user,
+ // db password, db name and db host
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 7)
+ })
+
+ it('Changes fields from sqlite to mysql then oci', () => {
+ const config = {
+ ...defaultConfig,
+ databases: {
+ sqlite: 'SQLite',
+ mysql: 'MySQL/MariaDB',
+ pgsql: 'PostgreSQL',
+ oci: 'Oracle',
+ },
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // SQLite selected
+ cy.get('[data-cy-setup-form-field="dbtype-sqlite"]')
+ .should('be.visible')
+ .find('input')
+ .should('be.checked')
+
+ // Admin login, password, data directory and 4 DB radio selectors
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 7)
+
+ // Change to MySQL
+ cy.get('[data-cy-setup-form-field="dbtype-mysql"]').click()
+ cy.get('[data-cy-setup-form-field="dbtype-mysql"] input').should('be.checked')
+
+ // Admin login, password, data directory, db user, db password,
+ // db name, db host and 4 DB radio selectors
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 11)
+
+ // Change to Oracle
+ cy.get('[data-cy-setup-form-field="dbtype-oci"]').click()
+ cy.get('[data-cy-setup-form-field="dbtype-oci"] input').should('be.checked')
+
+ // Admin login, password, data directory, db user, db password,
+ // db name, db table space, db host and 4 DB radio selectors
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 12)
+ cy.get('[data-cy-setup-form-field="dbtablespace"]')
+ .should('be.visible')
+ })
+})
+
+describe('Setup page with errors and warning', () => {
+ beforeEach(() => {
+ cy.mockInitialState('core', 'links', links)
+ })
+
+ afterEach(() => cy.unmockInitialState())
+
+ it('Renders error from backend', () => {
+ const config = {
+ ...defaultConfig,
+ errors: [
+ {
+ error: 'Error message',
+ hint: 'Error hint',
+ },
+ ],
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // Error message and hint
+ cy.get('[data-cy-setup-form-note="error"]')
+ .should('be.visible')
+ .should('have.length', 1)
+ .should('contain', 'Error message')
+ .should('contain', 'Error hint')
+ })
+
+ it('Renders errors from backend', () => {
+ const config = {
+ ...defaultConfig,
+ errors: [
+ 'Error message 1',
+ {
+ error: 'Error message',
+ hint: 'Error hint',
+ },
+ ],
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // Error message and hint
+ cy.get('[data-cy-setup-form-note="error"]')
+ .should('be.visible')
+ .should('have.length', 2)
+ cy.get('[data-cy-setup-form-note="error"]').eq(0)
+ .should('contain', 'Error message 1')
+ cy.get('[data-cy-setup-form-note="error"]').eq(1)
+ .should('contain', 'Error message')
+ .should('contain', 'Error hint')
+ })
+
+ it('Renders all the submitted fields on error', () => {
+ const config = {
+ ...defaultConfig,
+ adminlogin: 'admin',
+ adminpass: 'password',
+ dbname: 'nextcloud',
+ dbtype: 'mysql',
+ dbuser: 'nextcloud',
+ dbpass: 'password',
+ dbhost: 'localhost',
+ directory: '/var/www/html/nextcloud',
+ } as SetupConfig
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ cy.get('input[data-cy-setup-form-field="adminlogin"]')
+ .should('have.value', 'admin')
+ cy.get('input[data-cy-setup-form-field="adminpass"]')
+ .should('have.value', 'password')
+ cy.get('[data-cy-setup-form-field="dbtype-mysql"] input')
+ .should('be.checked')
+ cy.get('input[data-cy-setup-form-field="dbname"]')
+ .should('have.value', 'nextcloud')
+ cy.get('input[data-cy-setup-form-field="dbuser"]')
+ .should('have.value', 'nextcloud')
+ cy.get('input[data-cy-setup-form-field="dbpass"]')
+ .should('have.value', 'password')
+ cy.get('input[data-cy-setup-form-field="dbhost"]')
+ .should('have.value', 'localhost')
+ cy.get('input[data-cy-setup-form-field="directory"]')
+ .should('have.value', '/var/www/html/nextcloud')
+ })
+
+ it('Renders the htaccess warning', () => {
+ const config = {
+ ...defaultConfig,
+ htaccessWorking: false,
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ cy.get('[data-cy-setup-form-note="htaccess"]')
+ .should('be.visible')
+ .should('contain', 'Security warning')
+ .invoke('html')
+ .should('contains', links.adminInstall)
+ })
+})
+
+describe('Setup page with autoconfig', () => {
+ beforeEach(() => {
+ cy.mockInitialState('core', 'links', links)
+ })
+
+ afterEach(() => cy.unmockInitialState())
+
+ it('Renders autoconfig', () => {
+ const config = {
+ ...defaultConfig,
+ hasAutoconfig: true,
+ dbname: 'nextcloud',
+ dbtype: 'mysql',
+ dbuser: 'nextcloud',
+ dbpass: 'password',
+ dbhost: 'localhost',
+ directory: '/var/www/html/nextcloud',
+ } as SetupConfig
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // Autoconfig info note
+ cy.get('[data-cy-setup-form-note="autoconfig"]')
+ .should('be.visible')
+ .should('contain', 'Autoconfig file detected')
+
+ // Database and storage section is hidden as already set in autoconfig
+ cy.get('[data-cy-setup-form-advanced-config]').should('be.visible')
+ .invoke('attr', 'open')
+ .should('equal', undefined)
+
+ // Oracle tablespace is hidden
+ cy.get('[data-cy-setup-form-field="dbtablespace"]')
+ .should('not.exist')
+ })
+})
+
+describe('Submit a full form sends the data', () => {
+ beforeEach(() => {
+ cy.mockInitialState('core', 'links', links)
+ })
+
+ afterEach(() => cy.unmockInitialState())
+
+ it('Submits a full form', () => {
+ const config = {
+ ...defaultConfig,
+ adminlogin: 'admin',
+ adminpass: 'password',
+ dbname: 'nextcloud',
+ dbtype: 'mysql',
+ dbuser: 'nextcloud',
+ dbpass: 'password',
+ dbhost: 'localhost',
+ dbtablespace: 'tablespace',
+ directory: '/var/www/html/nextcloud',
+ } as SetupConfig
+
+ cy.intercept('POST', '**', {
+ delay: 2000,
+ }).as('setup')
+
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // Not chaining breaks the test as the POST prevents the element from being retrieved twice
+ // eslint-disable-next-line cypress/unsafe-to-chain-command
+ cy.get('[data-cy-setup-form-submit]')
+ .click()
+ .invoke('attr', 'disabled')
+ .should('equal', 'disabled', { timeout: 500 })
+
+ cy.wait('@setup')
+ .its('request.body')
+ .should('deep.equal', new URLSearchParams({
+ adminlogin: 'admin',
+ adminpass: 'password',
+ directory: '/var/www/html/nextcloud',
+ dbtype: 'mysql',
+ dbuser: 'nextcloud',
+ dbpass: 'password',
+ dbname: 'nextcloud',
+ dbhost: 'localhost',
+ }).toString())
+ })
+})
diff --git a/core/src/views/Setup.vue b/core/src/views/Setup.vue
new file mode 100644
index 00000000000..50ec0da9035
--- /dev/null
+++ b/core/src/views/Setup.vue
@@ -0,0 +1,460 @@
+<!--
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <form ref="form"
+ class="setup-form"
+ :class="{ 'setup-form--loading': loading }"
+ action=""
+ data-cy-setup-form
+ method="POST"
+ @submit="onSubmit">
+ <!-- Autoconfig info -->
+ <NcNoteCard v-if="config.hasAutoconfig"
+ :heading="t('core', 'Autoconfig file detected')"
+ data-cy-setup-form-note="autoconfig"
+ type="success">
+ {{ t('core', 'The setup form below is pre-filled with the values from the config file.') }}
+ </NcNoteCard>
+
+ <!-- Htaccess warning -->
+ <NcNoteCard v-if="config.htaccessWorking === false"
+ :heading="t('core', 'Security warning')"
+ data-cy-setup-form-note="htaccess"
+ type="warning">
+ <p v-html="htaccessWarning" />
+ </NcNoteCard>
+
+ <!-- Various errors -->
+ <NcNoteCard v-for="(error, index) in errors"
+ :key="index"
+ :heading="error.heading"
+ data-cy-setup-form-note="error"
+ type="error">
+ {{ error.message }}
+ </NcNoteCard>
+
+ <!-- Admin creation -->
+ <fieldset class="setup-form__administration">
+ <legend>{{ t('core', 'Create administration account') }}</legend>
+
+ <!-- Username -->
+ <NcTextField v-model="config.adminlogin"
+ :label="t('core', 'Administration account name')"
+ data-cy-setup-form-field="adminlogin"
+ name="adminlogin"
+ required />
+
+ <!-- Password -->
+ <NcPasswordField v-model="config.adminpass"
+ :label="t('core', 'Administration account password')"
+ data-cy-setup-form-field="adminpass"
+ name="adminpass"
+ required />
+
+ <!-- Password entropy -->
+ <NcNoteCard v-show="config.adminpass !== ''" :type="passwordHelperType">
+ {{ passwordHelperText }}
+ </NcNoteCard>
+ </fieldset>
+
+ <!-- Autoconfig toggle -->
+ <details :open="!isValidAutoconfig" data-cy-setup-form-advanced-config>
+ <summary>{{ t('core', 'Storage & database') }}</summary>
+
+ <!-- Data folder -->
+ <fieldset class="setup-form__data-folder">
+ <NcTextField v-model="config.directory"
+ :label="t('core', 'Data folder')"
+ :placeholder="config.serverRoot + '/data'"
+ required
+ autocomplete="off"
+ autocapitalize="none"
+ data-cy-setup-form-field="directory"
+ name="directory"
+ spellcheck="false" />
+ </fieldset>
+
+ <!-- Database -->
+ <fieldset class="setup-form__database">
+ <legend>{{ t('core', 'Database configuration') }}</legend>
+
+ <!-- Database type select -->
+ <fieldset class="setup-form__database-type">
+ <p v-if="!firstAndOnlyDatabase" :class="`setup-form__database-type-select--${DBTypeGroupDirection}`" class="setup-form__database-type-select">
+ <NcCheckboxRadioSwitch v-for="(name, db) in config.databases"
+ :key="db"
+ v-model="config.dbtype"
+ :button-variant="true"
+ :data-cy-setup-form-field="`dbtype-${db}`"
+ :value="db"
+ :button-variant-grouped="DBTypeGroupDirection"
+ name="dbtype"
+ type="radio">
+ {{ name }}
+ </NcCheckboxRadioSwitch>
+ </p>
+
+ <NcNoteCard v-else data-cy-setup-form-db-note="single-db" type="warning">
+ {{ t('core', 'Only {firstAndOnlyDatabase} is available.', { firstAndOnlyDatabase }) }}<br>
+ {{ t('core', 'Install and activate additional PHP modules to choose other database types.') }}<br>
+ <a :href="links.adminSourceInstall" target="_blank" rel="noreferrer noopener">
+ {{ t('core', 'For more details check out the documentation.') }} ↗
+ </a>
+ </NcNoteCard>
+
+ <NcNoteCard v-if="config.dbtype === 'sqlite'"
+ :heading="t('core', 'Performance warning')"
+ data-cy-setup-form-db-note="sqlite"
+ type="warning">
+ {{ t('core', 'You chose SQLite as database.') }}<br>
+ {{ t('core', 'SQLite should only be used for minimal and development instances. For production we recommend a different database backend.') }}<br>
+ {{ t('core', 'If you use clients for file syncing, the use of SQLite is highly discouraged.') }}
+ </NcNoteCard>
+ </fieldset>
+
+ <!-- Database configuration -->
+ <fieldset v-if="config.dbtype !== 'sqlite'">
+ <NcTextField v-model="config.dbuser"
+ :label="t('core', 'Database user')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbuser"
+ name="dbuser"
+ spellcheck="false"
+ required />
+
+ <NcPasswordField v-model="config.dbpass"
+ :label="t('core', 'Database password')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbpass"
+ name="dbpass"
+ spellcheck="false"
+ required />
+
+ <NcTextField v-model="config.dbname"
+ :label="t('core', 'Database name')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbname"
+ name="dbname"
+ pattern="[0-9a-zA-Z\$_\-]+"
+ spellcheck="false"
+ required />
+
+ <NcTextField v-if="config.dbtype === 'oci'"
+ v-model="config.dbtablespace"
+ :label="t('core', 'Database tablespace')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbtablespace"
+ name="dbtablespace"
+ spellcheck="false" />
+
+ <NcTextField v-model="config.dbhost"
+ :helper-text="t('core', 'Please specify the port number along with the host name (e.g., localhost:5432).')"
+ :label="t('core', 'Database host')"
+ :placeholder="t('core', 'localhost')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbhost"
+ name="dbhost"
+ spellcheck="false" />
+ </fieldset>
+ </fieldset>
+ </details>
+
+ <!-- Submit -->
+ <NcButton class="setup-form__button"
+ :class="{ 'setup-form__button--loading': loading }"
+ :disabled="loading"
+ :loading="loading"
+ :wide="true"
+ alignment="center-reverse"
+ data-cy-setup-form-submit
+ native-type="submit"
+ type="primary">
+ <template #icon>
+ <NcLoadingIcon v-if="loading" />
+ <IconArrowRight v-else />
+ </template>
+ {{ loading ? t('core', 'Installing …') : t('core', 'Install') }}
+ </NcButton>
+
+ <!-- Help note -->
+ <NcNoteCard data-cy-setup-form-note="help" type="info">
+ {{ t('core', 'Need help?') }}
+ <a target="_blank" rel="noreferrer noopener" :href="links.adminInstall">{{ t('core', 'See the documentation') }} ↗</a>
+ </NcNoteCard>
+ </form>
+</template>
+<script lang="ts">
+import type { DbType, SetupConfig, SetupLinks } from '../install'
+
+import { defineComponent } from 'vue'
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import DomPurify from 'dompurify'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+
+import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
+
+enum PasswordStrength {
+ VeryWeak,
+ Weak,
+ Moderate,
+ Strong,
+ VeryStrong,
+ ExtremelyStrong,
+}
+
+const checkPasswordEntropy = (password: string = ''): PasswordStrength => {
+ const uniqueCharacters = new Set(password)
+ const entropy = parseInt(Math.log2(Math.pow(parseInt(uniqueCharacters.size.toString()), password.length)).toFixed(2))
+ if (entropy < 16) {
+ return PasswordStrength.VeryWeak
+ } else if (entropy < 31) {
+ return PasswordStrength.Weak
+ } else if (entropy < 46) {
+ return PasswordStrength.Moderate
+ } else if (entropy < 61) {
+ return PasswordStrength.Strong
+ } else if (entropy < 76) {
+ return PasswordStrength.VeryStrong
+ }
+
+ return PasswordStrength.ExtremelyStrong
+}
+
+export default defineComponent({
+ name: 'Setup',
+
+ components: {
+ IconArrowRight,
+ NcButton,
+ NcCheckboxRadioSwitch,
+ NcLoadingIcon,
+ NcNoteCard,
+ NcPasswordField,
+ NcTextField,
+ },
+
+ setup() {
+ return {
+ t,
+ }
+ },
+
+ data() {
+ return {
+ config: {} as SetupConfig,
+ links: {} as SetupLinks,
+ isValidAutoconfig: false,
+ loading: false,
+ }
+ },
+
+ computed: {
+ passwordHelperText(): string {
+ if (this.config?.adminpass === '') {
+ return ''
+ }
+
+ const passwordStrength = checkPasswordEntropy(this.config?.adminpass)
+ switch (passwordStrength) {
+ case PasswordStrength.VeryWeak:
+ return t('core', 'Password is too weak')
+ case PasswordStrength.Weak:
+ return t('core', 'Password is weak')
+ case PasswordStrength.Moderate:
+ return t('core', 'Password is average')
+ case PasswordStrength.Strong:
+ return t('core', 'Password is strong')
+ case PasswordStrength.VeryStrong:
+ return t('core', 'Password is very strong')
+ case PasswordStrength.ExtremelyStrong:
+ return t('core', 'Password is extremely strong')
+ }
+
+ return t('core', 'Unknown password strength')
+ },
+ passwordHelperType() {
+ if (checkPasswordEntropy(this.config?.adminpass) < PasswordStrength.Moderate) {
+ return 'error'
+ }
+ if (checkPasswordEntropy(this.config?.adminpass) < PasswordStrength.Strong) {
+ return 'warning'
+ }
+ return 'success'
+ },
+
+ firstAndOnlyDatabase(): string|null {
+ const dbNames = Object.values(this.config?.databases || {})
+ if (dbNames.length === 1) {
+ return dbNames[0]
+ }
+
+ return null
+ },
+
+ DBTypeGroupDirection() {
+ const databases = Object.keys(this.config?.databases || {})
+ // If we have more than 3 databases, we want to display them vertically
+ if (databases.length > 3) {
+ return 'vertical'
+ }
+ return 'horizontal'
+ },
+
+ htaccessWarning(): string {
+ // We use v-html, let's make sure we're safe
+ const message = [
+ t('core', 'Your data directory and files are probably accessible from the internet because the <code>.htaccess</code> file does not work.'),
+ t('core', 'For information how to properly configure your server, please {linkStart}see the documentation{linkEnd}', {
+ linkStart: '<a href="' + this.links.adminInstall + '" target="_blank" rel="noreferrer noopener">',
+ linkEnd: '</a>',
+ }, { escape: false }),
+ ].join('<br>')
+ return DomPurify.sanitize(message)
+ },
+
+ errors() {
+ return (this.config?.errors || []).map(error => {
+ if (typeof error === 'string') {
+ return {
+ heading: '',
+ message: error,
+ }
+ }
+
+ // f no hint is set, we don't want to show a heading
+ if (error.hint === '') {
+ return {
+ heading: '',
+ message: error.error,
+ }
+ }
+
+ return {
+ heading: error.error,
+ message: error.hint,
+ }
+ })
+ },
+ },
+
+ beforeMount() {
+ // Needs to only read the state once we're mounted
+ // for Cypress to be properly initialized.
+ this.config = loadState<SetupConfig>('core', 'config')
+ this.links = loadState<SetupLinks>('core', 'links')
+
+ },
+
+ mounted() {
+ // Set the first database type as default if none is set
+ if (this.config.dbtype === '') {
+ this.config.dbtype = Object.keys(this.config.databases).at(0) as DbType
+ }
+
+ // Validate the legitimacy of the autoconfig
+ if (this.config.hasAutoconfig) {
+ const form = this.$refs.form as HTMLFormElement
+
+ // Check the form without the administration account fields
+ form.querySelectorAll('input[name="adminlogin"], input[name="adminpass"]').forEach(input => {
+ input.removeAttribute('required')
+ })
+
+ if (form.checkValidity() && this.config.errors.length === 0) {
+ this.isValidAutoconfig = true
+ } else {
+ this.isValidAutoconfig = false
+ }
+
+ // Restore the required attribute
+ // Check the form without the administration account fields
+ form.querySelectorAll('input[name="adminlogin"], input[name="adminpass"]').forEach(input => {
+ input.setAttribute('required', 'true')
+ })
+ }
+ },
+
+ methods: {
+ async onSubmit() {
+ this.loading = true
+ },
+ },
+})
+</script>
+<style lang="scss">
+form {
+ padding: calc(3 * var(--default-grid-baseline));
+ color: var(--color-main-text);
+ border-radius: var(--border-radius-container);
+ background-color: var(--color-main-background-blur);
+ box-shadow: 0 0 10px var(--color-box-shadow);
+ -webkit-backdrop-filter: var(--filter-background-blur);
+ backdrop-filter: var(--filter-background-blur);
+
+ max-width: 300px;
+ margin-bottom: 30px;
+
+ > fieldset:first-child,
+ > .notecard:first-child {
+ margin-top: 0;
+ }
+
+ > .notecard:last-child {
+ margin-bottom: 0;
+ }
+
+ fieldset,
+ details {
+ margin-block: 1rem;
+ }
+
+ .setup-form__button:not(.setup-form__button--loading) {
+ .material-design-icon {
+ transition: all linear var(--animation-quick);
+ }
+
+ &:hover .material-design-icon {
+ transform: translateX(0.2em);
+ }
+ }
+
+ // Db select required styling
+ .setup-form__database-type-select {
+ display: flex;
+ &--vertical {
+ flex-direction: column;
+ }
+ }
+
+}
+
+code {
+ background-color: var(--color-background-dark);
+ margin-top: 1rem;
+ padding: 0 0.3em;
+ border-radius: var(--border-radius);
+}
+
+// Various overrides
+.input-field {
+ margin-block-start: 1rem !important;
+}
+
+.notecard__heading {
+ font-size: inherit !important;
+}
+</style>
diff --git a/core/src/views/UnifiedSearch.vue b/core/src/views/UnifiedSearch.vue
index 38b18814665..d7b2ca634eb 100644
--- a/core/src/views/UnifiedSearch.vue
+++ b/core/src/views/UnifiedSearch.vue
@@ -3,16 +3,14 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
- <div class="header-menu unified-search-menu">
- <NcButton v-show="!showLocalSearch"
- class="header-menu__trigger"
+ <div class="unified-search-menu">
+ <NcHeaderButton v-show="!showLocalSearch"
:aria-label="t('core', 'Unified search')"
- type="tertiary-no-background"
@click="toggleUnifiedSearch">
<template #icon>
- <Magnify class="header-menu__trigger-icon" :size="20" />
+ <NcIconSvgWrapper :path="mdiMagnify" />
</template>
- </NcButton>
+ </NcHeaderButton>
<UnifiedSearchLocalSearchBar v-if="supportsLocalSearch"
:open.sync="showLocalSearch"
:query.sync="queryText"
@@ -24,25 +22,24 @@
</template>
<script lang="ts">
+import { mdiMagnify } from '@mdi/js'
import { emit, subscribe } from '@nextcloud/event-bus'
-import { translate } from '@nextcloud/l10n'
+import { t } from '@nextcloud/l10n'
import { useBrowserLocation } from '@vueuse/core'
+import debounce from 'debounce'
import { defineComponent } from 'vue'
-
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import Magnify from 'vue-material-design-icons/Magnify.vue'
+import NcHeaderButton from '@nextcloud/vue/components/NcHeaderButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import UnifiedSearchModal from '../components/UnifiedSearch/UnifiedSearchModal.vue'
import UnifiedSearchLocalSearchBar from '../components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue'
-
-import debounce from 'debounce'
-import logger from '../logger'
+import logger from '../logger.js'
export default defineComponent({
name: 'UnifiedSearch',
components: {
- NcButton,
- Magnify,
+ NcHeaderButton,
+ NcIconSvgWrapper,
UnifiedSearchModal,
UnifiedSearchLocalSearchBar,
},
@@ -52,7 +49,9 @@ export default defineComponent({
return {
currentLocation,
- t: translate,
+
+ mdiMagnify,
+ t,
}
},
@@ -175,31 +174,9 @@ export default defineComponent({
<style lang="scss" scoped>
// this is needed to allow us overriding component styles (focus-visible)
-#header {
- .header-menu {
- display: flex;
- align-items: center;
- justify-content: center;
-
- &__trigger {
- height: var(--header-height);
- width: var(--header-height) !important;
-
- &:focus-visible {
- // align with other header menu entries
- outline: none !important;
- box-shadow: none !important;
- }
-
- &:not(:hover,:focus,:focus-visible) {
- opacity: .85;
- }
-
- &-icon {
- // ensure the icon has the correct color
- color: var(--color-background-plain-text) !important;
- }
- }
- }
+.unified-search-menu {
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
</style>
diff --git a/core/src/views/UnsupportedBrowser.vue b/core/src/views/UnsupportedBrowser.vue
index d8d7dc55208..408cccf61e9 100644
--- a/core/src/views/UnsupportedBrowser.vue
+++ b/core/src/views/UnsupportedBrowser.vue
@@ -36,8 +36,8 @@ import { agents } from 'caniuse-lite/dist/unpacker/agents.js'
import { generateUrl, getRootUrl } from '@nextcloud/router'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import Web from 'vue-material-design-icons/Web.vue'
import { browserStorageKey } from '../utils/RedirectUnsupportedBrowsers.js'