refactor: use useSearch in views

This commit is contained in:
axolotle 2024-07-09 17:07:39 +02:00
parent 501bce484a
commit 8bb2451e9c
7 changed files with 122 additions and 155 deletions

View file

@ -7,8 +7,10 @@ import CardDeckFeed from '@/components/CardDeckFeed.vue'
import { useForm } from '@/composables/form' import { useForm } from '@/composables/form'
import { useAutoModal } from '@/composables/useAutoModal' import { useAutoModal } from '@/composables/useAutoModal'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { useSearch } from '@/composables/useSearch'
import { randint } from '@/helpers/commons' import { randint } from '@/helpers/commons'
import { appRepoUrl, required } from '@/helpers/validators' import { appRepoUrl, required } from '@/helpers/validators'
import type { Obj } from '@/types/commons'
import type { FieldProps, FormFieldDict } from '@/types/form' import type { FieldProps, FormFieldDict } from '@/types/form'
const props = withDefaults( const props = withDefaults(
@ -35,10 +37,42 @@ const { loading } = useInitialQueries(
{ onQueriesResponse }, { onQueriesResponse },
) )
const apps = ref()
const selectedApp = ref() const selectedApp = ref()
const antifeatures = ref() const antifeatures = ref()
const apps = ref<Obj[] | undefined>()
const [search, filteredApps] = useSearch(
apps,
(s, app) => {
// app doesn't match quality filter
if (props.quality !== 'all' && !app[props.quality]) return false
// app doesn't match category filter
if (props.category !== 'all' && app.category !== props.category)
return false
if (props.subtag !== 'all') {
const appMatchSubtag =
props.subtag === 'others'
? app.subtags.length === 0
: app.subtags.includes(props.subtag)
// app doesn't match subtag filter
if (!appMatchSubtag) return false
}
if (s === '') return true
if (app.searchValues.includes(search)) return true
return false
},
{
externalSearch: () => props.search,
filterIfNoSearch: true,
filterAllFn(s) {
if (props.category === null) return false
if (props.quality === 'all' && props.category === 'all' && s === '') {
return true
}
},
},
)
const form = ref({ url: '' }) const form = ref({ url: '' })
const fields = { const fields = {
url: { url: {
@ -68,34 +102,6 @@ const categories = reactive([
// The rest is filled from api data // The rest is filled from api data
]) ])
const filteredApps = computed(() => {
if (!apps.value || props.category === null) return
const search = props.search.toLowerCase()
if (props.quality === 'all' && props.category === 'all' && search === '') {
return apps.value
}
const filtered = apps.value.filter((app) => {
// app doesn't match quality filter
if (props.quality !== 'all' && !app[props.quality]) return false
// app doesn't match category filter
if (props.category !== 'all' && app.category !== props.category)
return false
if (props.subtag !== 'all') {
const appMatchSubtag =
props.subtag === 'others'
? app.subtags.length === 0
: app.subtags.includes(props.subtag)
// app doesn't match subtag filter
if (!appMatchSubtag) return false
}
if (search === '') return true
if (app.searchValues.includes(search)) return true
return false
})
return filtered.length ? filtered : null
})
const subtags = computed(() => { const subtags = computed(() => {
// build an options array for subtags v-model/options // build an options array for subtags v-model/options
if (props.category && categories.length > 2) { if (props.category && categories.length > 2) {
@ -196,8 +202,8 @@ const onCustomInstallClick = onSubmit(async () => {
<template> <template>
<ViewSearch <ViewSearch
:filtered-items="filteredApps" v-model="search"
:items="apps" :items="filteredApps"
items-name="apps" items-name="apps"
:loading="loading" :loading="loading"
> >
@ -211,15 +217,15 @@ const onCustomInstallClick = onSubmit(async () => {
<BFormInput <BFormInput
id="search-input" id="search-input"
:model-value="search"
:placeholder="$t('search.for', { items: $t('items.apps', 2) })" :placeholder="$t('search.for', { items: $t('items.apps', 2) })"
:modelValue="search" @update:model-value="updateQuery('search', $event)"
@update:modelValue="updateQuery('search', $event)"
/> />
<BFormSelect <BFormSelect
:modelValue="quality" :model-value="quality"
:options="qualityOptions" :options="qualityOptions"
@update:modelValue="updateQuery('quality', $event)" @update:model-value="updateQuery('quality', $event)"
/> />
</BInputGroup> </BInputGroup>
@ -230,9 +236,9 @@ const onCustomInstallClick = onSubmit(async () => {
</BInputGroupText> </BInputGroupText>
<BFormSelect <BFormSelect
:modelValue="category" :model-value="category"
:options="categories" :options="categories"
@update:modelValue="updateQuery('category', $event)" @update:model-value="updateQuery('category', $event)"
/> />
<BButton <BButton
@ -260,9 +266,9 @@ const onCustomInstallClick = onSubmit(async () => {
<BFormSelect <BFormSelect
id="subtags-select" id="subtags-select"
:modelValue="subtag" :model-value="subtag"
:options="subtags" :options="subtags"
@update:modelValue="updateQuery('subtag', $event)" @update:model-value="updateQuery('subtag', $event)"
/> />
</BInputGroup> </BInputGroup>
</div> </div>

View file

@ -1,29 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { ref } from 'vue'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { useSearch } from '@/composables/useSearch'
import type { Obj } from '@/types/commons'
const { loading } = useInitialQueries([['GET', 'apps?full']], { const { loading } = useInitialQueries([['GET', 'apps?full']], {
onQueriesResponse, onQueriesResponse,
}) })
const search = ref('')
const apps = ref()
const filteredApps = computed(() => { const apps = ref<Obj[] | undefined>()
if (!apps.value) return const [search, filteredApps] = useSearch(apps, (s, app) => {
const search_ = search.value.toLowerCase() return Object.values(app).some(
// Check if any value in apps (label, id, name, description) match the search query. (value) => value && value.toLowerCase().includes(s),
const filtered = apps.value.filter((app) =>
Object.values(app).some(
(item) => item && item.toLowerCase().includes(search_),
),
) )
return filtered.length ? filtered : null
}) })
function onQueriesResponse(data: any) { function onQueriesResponse(data: any) {
if (data.apps.length === 0) { if (data.apps.length === 0) {
apps.value = null apps.value = undefined
return return
} }
@ -39,10 +34,9 @@ function onQueriesResponse(data: any) {
<template> <template>
<ViewSearch <ViewSearch
v-model:search="search" v-model="search"
:filtered-items="filteredApps"
items-name="installed_apps" items-name="installed_apps"
:items="apps" :items="filteredApps"
:loading="loading" :loading="loading"
> >
<template #top-bar-buttons> <template #top-bar-buttons>

View file

@ -1,41 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStoreGetters } from '@/store/utils' import { useStoreGetters } from '@/store/utils'
import { computed, ref } from 'vue'
import RecursiveListGroup from '@/components/RecursiveListGroup.vue' import RecursiveListGroup from '@/components/RecursiveListGroup.vue'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { useSearch } from '@/composables/useSearch'
import type { TreeRootNode } from '@/helpers/data/tree' import type { TreeRootNode } from '@/helpers/data/tree'
import type { ComputedRef } from 'vue'
const { domains, mainDomain, domainsTree } = useStoreGetters() const { mainDomain, domainsTree } = useStoreGetters()
const { loading } = useInitialQueries([ const { loading } = useInitialQueries([
['GET', { uri: 'domains', storeKey: 'domains' }], ['GET', { uri: 'domains', storeKey: 'domains' }],
]) ])
const search = ref('') const [search, filteredTree] = useSearch(
const tree = computed(() => {
// FIXME rm ts type when moved to pinia or else // FIXME rm ts type when moved to pinia or else
const tree = domainsTree.value as TreeRootNode | undefined domainsTree as ComputedRef<TreeRootNode | undefined>,
if (!tree) return (s, node) => node.id.includes(s),
const search_ = search.value.toLowerCase() )
if (search_) {
return tree.filter((node) => node.id.includes(search_))
}
return tree
})
const hasFilteredItems = computed(() => {
if (!tree.value) return null
return tree.value.children.length ? tree.value.children : null
})
</script> </script>
<template> <template>
<ViewSearch <ViewSearch
id="domain-list" id="domain-list"
v-model:search="search" v-model="search"
:filtered-items="hasFilteredItems" :items="filteredTree ? filteredTree.children : filteredTree"
:items="domains"
items-name="domains" items-name="domains"
:loading="loading" :loading="loading"
> >
@ -47,8 +35,8 @@ const hasFilteredItems = computed(() => {
</template> </template>
<RecursiveListGroup <RecursiveListGroup
v-if="tree" v-if="filteredTree"
:tree="tree" :tree="filteredTree"
:toggle-text="$t('domain.toggle_subdomains')" :toggle-text="$t('domain.toggle_subdomains')"
class="mb-5" class="mb-5"
> >
@ -67,9 +55,9 @@ const hasFilteredItems = computed(() => {
<small <small
v-if="data.name === mainDomain" v-if="data.name === mainDomain"
v-b-tooltip.hover
:title="$t('domain.types.main_domain')" :title="$t('domain.types.main_domain')"
class="ms-1" class="ms-1"
v-b-tooltip.hover
> >
<YIcon iname="star" /> <YIcon iname="star" />
</small> </small>

View file

@ -1,12 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import api from '@/api' import api from '@/api'
import TagsSelectizeItem from '@/components/globals/formItems/TagsSelectizeItem.vue' import TagsSelectizeItem from '@/components/globals/formItems/TagsSelectizeItem.vue'
import { useAutoModal } from '@/composables/useAutoModal' import { useAutoModal } from '@/composables/useAutoModal'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { useSearch } from '@/composables/useSearch'
import { isEmptyValue } from '@/helpers/commons' import { isEmptyValue } from '@/helpers/commons'
import type { Obj } from '@/types/commons'
// TODO add global search with type (search by: group, user, permission) // TODO add global search with type (search by: group, user, permission)
// TODO add vuex store update on inputs ? // TODO add vuex store update on inputs ?
@ -28,25 +30,15 @@ const { loading } = useInitialQueries(
{ onQueriesResponse }, { onQueriesResponse },
) )
const search = ref('')
const permissions = ref() const permissions = ref()
const permissionsOptions = ref() const permissionsOptions = ref()
const primaryGroups = ref() const primaryGroups = ref<Obj[] | undefined>()
const userGroups = ref() const userGroups = ref()
const usersOptions = ref() const usersOptions = ref()
const activeUserGroups = ref() const activeUserGroups = ref()
const filteredGroups = computed(() => { const [search, filteredGroups] = useSearch(primaryGroups, (s, group) => {
const groups = primaryGroups.value return group.name.toLowerCase().includes(s)
if (!groups) return
const search_ = search.value.toLowerCase()
const filtered = {}
for (const groupName in groups) {
if (groupName.toLowerCase().includes(search_)) {
filtered[groupName] = groups[groupName]
}
}
return isEmptyValue(filtered) ? null : filtered
}) })
function onQueriesResponse(users: any, allGroups: any, permsDict: any) { function onQueriesResponse(users: any, allGroups: any, permsDict: any) {
@ -57,12 +49,12 @@ function onQueriesResponse(users: any, allGroups: any, permsDict: any) {
...value, ...value,
})) }))
const userNames = users ? Object.keys(users) : [] const userNames = users ? Object.keys(users) : []
const primaryGroups_ = {} const primaryGroups_ = []
const userGroups_ = {} const userGroups_ = {}
for (const groupName in allGroups) { for (const groupName in allGroups) {
// copy the group to unlink it from the store // copy the group to unlink it from the store
const group_ = { ...allGroups[groupName] } const group_ = { ...allGroups[groupName], name: groupName }
group_.permissions = group_.permissions.map((perm) => { group_.permissions = group_.permissions.map((perm) => {
return permsDict[perm].label return permsDict[perm].label
}) })
@ -103,7 +95,7 @@ function onQueriesResponse(users: any, allGroups: any, permsDict: any) {
.map(({ id }) => permsDict[id].label) .map(({ id }) => permsDict[id].label)
} }
primaryGroups_[groupName] = group_ primaryGroups_.push(group_)
} }
const activeUserGroups_ = Object.entries(userGroups_) const activeUserGroups_ = Object.entries(userGroups_)
@ -179,16 +171,17 @@ async function deleteGroup(groupName) {
{ key: 'groups.delete', name: groupName }, { key: 'groups.delete', name: groupName },
) )
.then(() => { .then(() => {
delete primaryGroups.value[groupName] primaryGroups.value = primaryGroups.value?.filter(
(group) => group.name !== groupName,
)
}) })
} }
</script> </script>
<template> <template>
<ViewSearch <ViewSearch
v-model:search="search" v-model="search"
:filtered-items="filteredGroups" :items="filteredGroups"
:items="primaryGroups"
items-name="groups" items-name="groups"
:loading="loading" :loading="loading"
skeleton="CardFormSkeleton" skeleton="CardFormSkeleton"
@ -201,13 +194,13 @@ async function deleteGroup(groupName) {
<!-- PRIMARY GROUPS CARDS --> <!-- PRIMARY GROUPS CARDS -->
<YCard <YCard
v-for="(group, groupName) in filteredGroups" v-for="group in filteredGroups"
:key="groupName" :key="group.name"
collapsable collapsable
:title=" :title="
group.isSpecial group.isSpecial
? $t('group_' + groupName) ? $t('group_' + group.name)
: `${$t('group')} '${groupName}'` : `${$t('group')} '${group.name}'`
" "
icon="group" icon="group"
> >
@ -215,7 +208,7 @@ async function deleteGroup(groupName) {
<!-- DELETE GROUP --> <!-- DELETE GROUP -->
<BButton <BButton
v-if="!group.isSpecial" v-if="!group.isSpecial"
@click="deleteGroup(groupName)" @click="deleteGroup(group.name)"
size="sm" size="sm"
variant="danger" variant="danger"
> >
@ -231,23 +224,23 @@ async function deleteGroup(groupName) {
<template v-if="group.isSpecial"> <template v-if="group.isSpecial">
<p class="text-primary-emphasis"> <p class="text-primary-emphasis">
<YIcon iname="info-circle" /> <YIcon iname="info-circle" />
{{ $t('group_explain_' + groupName) }} {{ $t('group_explain_' + group.name) }}
</p> </p>
<p class="text-primary-emphasis" v-if="groupName === 'visitors'"> <p class="text-primary-emphasis" v-if="group.name === 'visitors'">
<em>{{ <em>{{
$t('group_explain_visitors_needed_for_external_client') $t('group_explain_visitors_needed_for_external_client')
}}</em> }}</em>
</p> </p>
</template> </template>
<template v-if="groupName == 'admins' || !group.isSpecial"> <template v-if="group.name == 'admins' || !group.isSpecial">
<TagsSelectizeItem <TagsSelectizeItem
v-model="group.members" v-model="group.members"
:options="usersOptions" :options="usersOptions"
:id="groupName + '-users'" :id="group.name + '-users'"
:label="$t('group_add_member')" :label="$t('group_add_member')"
tag-icon="user" tag-icon="user"
items-name="users" items-name="users"
@tag-update="onUserChanged({ ...$event, groupName })" @tag-update="onUserChanged({ ...$event, groupName: group.name })"
/> />
</template> </template>
</BCol> </BCol>
@ -262,11 +255,13 @@ async function deleteGroup(groupName) {
<TagsSelectizeItem <TagsSelectizeItem
v-model="group.permissions" v-model="group.permissions"
:options="permissionsOptions" :options="permissionsOptions"
:id="groupName + '-perms'" :id="group.name + '-perms'"
:label="$t('group_add_permission')" :label="$t('group_add_permission')"
tag-icon="key-modern" tag-icon="key-modern"
items-name="permissions" items-name="permissions"
@tag-update="onPermissionChanged({ ...$event, groupName })" @tag-update="
onPermissionChanged({ ...$event, groupName: group.name })
"
:disabled-items="group.disabledItems" :disabled-items="group.disabledItems"
/> />
</BCol> </BCol>

View file

@ -1,21 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { ref } from 'vue'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { useSearch } from '@/composables/useSearch'
import { distanceToNow } from '@/helpers/filters/date' import { distanceToNow } from '@/helpers/filters/date'
import type { Obj } from '@/types/commons'
const { loading } = useInitialQueries([['GET', 'services']], { const { loading } = useInitialQueries([['GET', 'services']], {
onQueriesResponse, onQueriesResponse,
}) })
const search = ref('')
const services = ref()
const filteredServices = computed(() => { const services = ref<Obj[] | undefined>()
if (!services.value) return const [search, filteredServices] = useSearch(services, (s, service) => {
const services_ = services.value.filter(({ name }) => { return service.name.toLowerCase().includes(s)
return name.toLowerCase().includes(search.value.toLowerCase())
})
return services_.length ? services_ : null
}) })
function onQueriesResponse(services_: any) { function onQueriesResponse(services_: any) {
@ -34,9 +31,8 @@ function onQueriesResponse(services_: any) {
<template> <template>
<ViewSearch <ViewSearch
id="service-list" id="service-list"
v-model:search="search" v-model="search"
:filtered-items="filteredServices" :items="filteredServices"
:items="services"
items-name="services" items-name="services"
:loading="loading" :loading="loading"
> >

View file

@ -1,24 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { ref } from 'vue'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { useSearch } from '@/composables/useSearch'
import { distanceToNow, readableDate } from '@/helpers/filters/date' import { distanceToNow, readableDate } from '@/helpers/filters/date'
import type { Obj } from '@/types/commons'
const { loading } = useInitialQueries( const { loading } = useInitialQueries(
[['GET', `logs?limit=${25}&with_details`]], [['GET', `logs?limit=${25}&with_details`]],
{ onQueriesResponse }, { onQueriesResponse },
) )
const search = ref('') const operations = ref<Obj[] | undefined>()
const operations = ref() const [search, filteredOperations] = useSearch(operations, (s, op) => {
return op.description.toLowerCase().includes(s)
const filteredOperations = computed(() => {
if (!operations.value) return
const search_ = search.value.toLowerCase()
const operations_ = operations.value.filter(({ description }) => {
return description.toLowerCase().includes(search_)
})
return operations_.length ? operations_ : null
}) })
function onQueriesResponse({ operation }: any) { function onQueriesResponse({ operation }: any) {
@ -40,9 +35,8 @@ function onQueriesResponse({ operation }: any) {
<template> <template>
<ViewSearch <ViewSearch
v-model:search="search" v-model="search"
:filtered-items="filteredOperations" :items="filteredOperations"
:items="operations"
items-name="logs" items-name="logs"
:loading="loading" :loading="loading"
skeleton="CardListSkeleton" skeleton="CardListSkeleton"

View file

@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { type ComputedRef } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { useInitialQueries } from '@/composables/useInitialQueries' import { useInitialQueries } from '@/composables/useInitialQueries'
import { useSearch } from '@/composables/useSearch'
import { useStoreGetters } from '@/store/utils' import { useStoreGetters } from '@/store/utils'
import type { Obj } from '@/types/commons'
const store = useStore() const store = useStore()
const { loading } = useInitialQueries([ const { loading } = useInitialQueries([
@ -15,20 +17,13 @@ const { loading } = useInitialQueries([
}, },
], ],
]) ])
const { users } = useStoreGetters()
const search = ref('') const { users } = useStoreGetters()
const filteredUsers = computed(() => { const [search, filteredUsers] = useSearch(
if (!users.value) return users as ComputedRef<Obj[] | undefined>,
const search_ = search.value.toLowerCase() (s, user) =>
const filtered = users.value.filter((user) => { user.username.toLowerCase().includes(s) || user.groups.includes(s),
return ( )
user.username.toLowerCase().includes(search_) ||
user.groups.includes(search_)
)
})
return filtered.length === 0 ? null : filtered
})
function downloadExport() { function downloadExport() {
const host = store.getters.host const host = store.getters.host
@ -38,9 +33,8 @@ function downloadExport() {
<template> <template>
<ViewSearch <ViewSearch
v-model:search="search" v-model="search"
:filtered-items="filteredUsers" :items="filteredUsers"
:items="users"
items-name="users" items-name="users"
:loading="loading" :loading="loading"
> >