Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions apps/settings/src/components/AppNavigationGroupList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@
</template>
</NcAppNavigationCaption>

<NcAppNavigationSearch
v-model="groupsSearchQuery"
:label="t('settings', 'Search groups…')" />

<p id="group-list-desc" class="hidden-visually">
{{ t('settings', 'List of groups. This list is not fully populated for performance reasons. The groups will be loaded as you navigate or search through the list.') }}
</p>
Expand Down Expand Up @@ -76,7 +72,6 @@ import NcActionInput from '@nextcloud/vue/components/NcActionInput'
import NcActionText from '@nextcloud/vue/components/NcActionText'
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList'
import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSearch'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import GroupListItem from './GroupListItem.vue'
Expand Down Expand Up @@ -123,8 +118,8 @@ const newGroupName = ref('')
const loadingGroups = ref(false)
/** Search offset */
const offset = ref(0)
/** Search query for groups */
const groupsSearchQuery = ref('')
/** Search query — shared via Vuex store */
const groupsSearchQuery = computed(() => store.getters.getSearchQuery)
const filteredGroups = computed(() => {
if (isAdminOrDelegatedAdmin.value) {
return userGroups.value
Expand Down
32 changes: 9 additions & 23 deletions apps/settings/src/components/UserList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
<script>
import { mdiAccountGroupOutline } from '@mdi/js'
import { showError } from '@nextcloud/dialogs'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import Vue from 'vue'
import { Fragment } from 'vue-frag'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
Expand Down Expand Up @@ -139,11 +138,14 @@ export default {

newUser: { ...newUser },
isInitialLoad: true,
searchQuery: '',
}
},

computed: {
searchQuery() {
return this.$store.getters.getSearchQuery
},

showConfig() {
return this.$store.getters.getShowConfig
},
Expand Down Expand Up @@ -224,6 +226,11 @@ export default {
},

watch: {
async searchQuery() {
this.$store.commit('resetUsers')
await this.loadUsers()
},

// watch url change and group select
async selectedGroup(val) {
this.isInitialLoad = true
Expand Down Expand Up @@ -253,23 +260,12 @@ export default {
*/
this.resetForm()

/**
* Register search
*/
subscribe('nextcloud:unified-search.search', this.search)
subscribe('nextcloud:unified-search.reset', this.resetSearch)

/**
* If disabled group but empty, redirect
*/
await this.redirectIfDisabled()
},

beforeDestroy() {
unsubscribe('nextcloud:unified-search.search', this.search)
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
},

methods: {
async handleScrollEnd() {
await this.loadUsers()
Expand Down Expand Up @@ -314,16 +310,6 @@ export default {
})
},

async search({ query }) {
this.searchQuery = query
this.$store.commit('resetUsers')
await this.loadUsers()
},

resetSearch() {
this.search({ query: '' })
},

resetForm() {
// revert form to original state
this.newUser = { ...newUser }
Expand Down
31 changes: 31 additions & 0 deletions apps/settings/src/components/Users/UserSettingsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,30 @@
taggable
@option:selected="setDefaultQuota" />
</NcAppSettingsSection>

<NcAppSettingsShortcutsSection>
<NcHotkeyList :label="t('settings', 'Search')">
<NcHotkey :label="t('settings', 'Focus search')" hotkey="Control F" />
</NcHotkeyList>
<NcHotkeyList :label="t('settings', 'Help')">
<NcHotkey :label="t('settings', 'Show those shortcuts')" hotkey="?" />
</NcHotkeyList>
</NcAppSettingsShortcutsSection>
</NcAppSettingsDialog>
</template>

<script>
import axios from '@nextcloud/axios'
import { formatFileSize, parseFileSize } from '@nextcloud/files'
import { generateUrl } from '@nextcloud/router'
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
import { nextTick } from 'vue'
import NcAppSettingsDialog from '@nextcloud/vue/components/NcAppSettingsDialog'
import NcAppSettingsSection from '@nextcloud/vue/components/NcAppSettingsSection'
import NcAppSettingsShortcutsSection from '@nextcloud/vue/components/NcAppSettingsShortcutsSection'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcHotkey from '@nextcloud/vue/components/NcHotkey'
import NcHotkeyList from '@nextcloud/vue/components/NcHotkeyList'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import { GroupSorting } from '../../constants/GroupManagement.ts'
Expand All @@ -125,7 +139,10 @@ export default {
components: {
NcAppSettingsDialog,
NcAppSettingsSection,
NcAppSettingsShortcutsSection,
NcCheckboxRadioSwitch,
NcHotkey,
NcHotkeyList,
NcNoteCard,
NcSelect,
},
Expand All @@ -137,6 +154,20 @@ export default {
},
},

emits: ['update:open'],

setup(_, { emit }) {
// ? opens the settings dialog on the keyboard shortcuts section
useHotKey('?', async () => {
emit('update:open', true)
await nextTick()
document.getElementById('settings-section_keyboard-shortcuts')?.scrollIntoView({
behavior: 'smooth',
inline: 'nearest',
})
}, { stop: true, prevent: true })
},

data() {
return {
selectedQuota: false,
Expand Down
16 changes: 8 additions & 8 deletions apps/settings/src/store/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const state = {
usersLimit: 25,
disabledUsersOffset: 0,
disabledUsersLimit: 25,
searchQuery: '',
userCount: usersSettings.userCount ?? 0,
showConfig: {
showStoragePath: usersSettings.showConfig?.user_list_show_storage_path,
Expand Down Expand Up @@ -237,6 +238,10 @@ const mutations = {
]
},

setSearchQuery(state, query) {
state.searchQuery = query
},

setShowConfig(state, { key, value }) {
state.showConfig[key] = value
},
Expand Down Expand Up @@ -266,6 +271,9 @@ const getters = {
getGroups(state) {
return state.groups
},
getSearchQuery(state) {
return state.searchQuery
},
getSubAdminGroups() {
return usersSettings.subAdminGroups ?? []
},
Expand Down Expand Up @@ -365,14 +373,6 @@ const actions = {
}
searchRequestCancelSource = CancelToken.source()
search = typeof search === 'string' ? search : ''

/**
* Adding filters in the search bar such as in:files, in:users, etc.
* collides with this particular search, so we need to remove them
* here and leave only the original search query
*/
search = search.replace(/in:[^\s]+/g, '').trim()

group = typeof group === 'string' ? group : ''
if (group !== '') {
return api.get(generateOcsUrl('cloud/groups/{group}/users/details?offset={offset}&limit={limit}&search={search}', { group: encodeURIComponent(group), offset, limit, search }), {
Expand Down
47 changes: 45 additions & 2 deletions apps/settings/src/views/UserManagementNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@
</template>
</NcAppNavigationNew>

<div class="account-management__search" role="search" :aria-label="t('settings', 'Search accounts and groups')">
<NcInputField
ref="searchField"
v-model="searchInput"
:label="t('settings', 'Search accounts and groups…')"
:show-trailing-button="searchInput !== ''"
:trailingButtonLabel="t('settings', 'Clear search')"
@trailing-button-click="clearSearch">
<template #icon>
<NcIconSvgWrapper :path="mdiMagnify" />
</template>
<template #trailing-button-icon>
<NcIconSvgWrapper :path="mdiClose" />
</template>
</NcInputField>
</div>

<NcAppNavigationList
class="account-management__system-list"
data-cy-users-settings-navigation-groups="system">
Expand Down Expand Up @@ -107,9 +124,11 @@
</template>

<script setup lang="ts">
import { mdiAccountOffOutline, mdiAccountOutline, mdiCogOutline, mdiHistory, mdiPlus, mdiShieldAccountOutline } from '@mdi/js'
import { mdiAccountOffOutline, mdiAccountOutline, mdiClose, mdiCogOutline, mdiHistory, mdiMagnify, mdiPlus, mdiShieldAccountOutline } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { computed, ref } from 'vue'
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
import debounce from 'debounce'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { useRoute } from 'vue-router/composables'
import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation'
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
Expand All @@ -118,6 +137,7 @@ import NcAppNavigationNew from '@nextcloud/vue/components/NcAppNavigationNew'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCounterBubble from '@nextcloud/vue/components/NcCounterBubble'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcInputField from '@nextcloud/vue/components/NcInputField'
import AppNavigationGroupList from '../components/AppNavigationGroupList.vue'
import UserSettingsDialog from '../components/Users/UserSettingsDialog.vue'
import { useFormatGroups } from '../composables/useGroupsNavigation.js'
Expand All @@ -126,6 +146,24 @@ import { useStore } from '../store/index.js'
const route = useRoute()
const store = useStore()

const searchField = ref<InstanceType<typeof NcInputField>>()
const searchInput = ref('')
const commitSearch = debounce((query: string) => {
store.commit('setSearchQuery', query)
}, 300)
watch(searchInput, (value) => commitSearch(value))
function clearSearch() {
commitSearch.clear()
searchInput.value = ''
store.commit('setSearchQuery', '')
}
onBeforeUnmount(() => commitSearch.clear())

// Intercept Ctrl/Cmd+F to focus the local search. useHotKey ignores the
// event when an input/textarea is already focused, so a second press falls
// through to the browser's native find-in-page.
useHotKey('f', () => searchField.value?.focus(), { ctrl: true, stop: true, prevent: true })

/** State of the 'new-account' dialog */
const isDialogOpen = ref(false)

Expand Down Expand Up @@ -163,6 +201,11 @@ function showNewUserMenu() {
will-change: scroll-position;
}
}
&__search {
padding-block: var(--default-grid-baseline, 4px);
padding-inline: var(--app-navigation-padding, 8px);
}

&__system-list {
height: auto !important;
overflow: visible !important;
Expand Down
16 changes: 15 additions & 1 deletion core/src/views/UnifiedSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,17 @@ export default defineComponent({
*/
supportsLocalSearch() {
// TODO: Make this an API
const providerPaths = ['/settings/users', '/apps/deck', '/settings/apps']
const providerPaths = ['/apps/deck', '/settings/apps']
return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
},
/**
* Current page handles the Ctrl+F shortcut itself (e.g. has a dedicated
* search input). UnifiedSearch should stay out of the way on these pages.
*/
appHandlesSearchShortcut() {
// TODO: Make this an API
const providerPaths = ['/settings/users']
return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
},
},
Expand Down Expand Up @@ -135,6 +145,10 @@ export default defineComponent({
*/
onKeyDown(event: KeyboardEvent) {
if (event.ctrlKey && event.key === 'f') {
// Skip on pages that handle Ctrl+F themselves (e.g. a dedicated search input).
if (this.appHandlesSearchShortcut) {
return
}
// only handle search if not already open - in this case the browser native search should be used
if (!this.showLocalSearch && !this.showUnifiedSearch) {
event.preventDefault()
Expand Down
Loading
Loading