mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
refactor: use useSearch in views
This commit is contained in:
parent
501bce484a
commit
8bb2451e9c
7 changed files with 122 additions and 155 deletions
|
@ -7,8 +7,10 @@ import CardDeckFeed from '@/components/CardDeckFeed.vue'
|
|||
import { useForm } from '@/composables/form'
|
||||
import { useAutoModal } from '@/composables/useAutoModal'
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { useSearch } from '@/composables/useSearch'
|
||||
import { randint } from '@/helpers/commons'
|
||||
import { appRepoUrl, required } from '@/helpers/validators'
|
||||
import type { Obj } from '@/types/commons'
|
||||
import type { FieldProps, FormFieldDict } from '@/types/form'
|
||||
|
||||
const props = withDefaults(
|
||||
|
@ -35,10 +37,42 @@ const { loading } = useInitialQueries(
|
|||
{ onQueriesResponse },
|
||||
)
|
||||
|
||||
const apps = ref()
|
||||
const selectedApp = 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 fields = {
|
||||
url: {
|
||||
|
@ -68,34 +102,6 @@ const categories = reactive([
|
|||
// 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(() => {
|
||||
// build an options array for subtags v-model/options
|
||||
if (props.category && categories.length > 2) {
|
||||
|
@ -196,8 +202,8 @@ const onCustomInstallClick = onSubmit(async () => {
|
|||
|
||||
<template>
|
||||
<ViewSearch
|
||||
:filtered-items="filteredApps"
|
||||
:items="apps"
|
||||
v-model="search"
|
||||
:items="filteredApps"
|
||||
items-name="apps"
|
||||
:loading="loading"
|
||||
>
|
||||
|
@ -211,15 +217,15 @@ const onCustomInstallClick = onSubmit(async () => {
|
|||
|
||||
<BFormInput
|
||||
id="search-input"
|
||||
:model-value="search"
|
||||
:placeholder="$t('search.for', { items: $t('items.apps', 2) })"
|
||||
:modelValue="search"
|
||||
@update:modelValue="updateQuery('search', $event)"
|
||||
@update:model-value="updateQuery('search', $event)"
|
||||
/>
|
||||
|
||||
<BFormSelect
|
||||
:modelValue="quality"
|
||||
:model-value="quality"
|
||||
:options="qualityOptions"
|
||||
@update:modelValue="updateQuery('quality', $event)"
|
||||
@update:model-value="updateQuery('quality', $event)"
|
||||
/>
|
||||
</BInputGroup>
|
||||
|
||||
|
@ -230,9 +236,9 @@ const onCustomInstallClick = onSubmit(async () => {
|
|||
</BInputGroupText>
|
||||
|
||||
<BFormSelect
|
||||
:modelValue="category"
|
||||
:model-value="category"
|
||||
:options="categories"
|
||||
@update:modelValue="updateQuery('category', $event)"
|
||||
@update:model-value="updateQuery('category', $event)"
|
||||
/>
|
||||
|
||||
<BButton
|
||||
|
@ -260,9 +266,9 @@ const onCustomInstallClick = onSubmit(async () => {
|
|||
|
||||
<BFormSelect
|
||||
id="subtags-select"
|
||||
:modelValue="subtag"
|
||||
:model-value="subtag"
|
||||
:options="subtags"
|
||||
@update:modelValue="updateQuery('subtag', $event)"
|
||||
@update:model-value="updateQuery('subtag', $event)"
|
||||
/>
|
||||
</BInputGroup>
|
||||
</div>
|
||||
|
|
|
@ -1,29 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { useSearch } from '@/composables/useSearch'
|
||||
import type { Obj } from '@/types/commons'
|
||||
|
||||
const { loading } = useInitialQueries([['GET', 'apps?full']], {
|
||||
onQueriesResponse,
|
||||
})
|
||||
const search = ref('')
|
||||
const apps = ref()
|
||||
|
||||
const filteredApps = computed(() => {
|
||||
if (!apps.value) return
|
||||
const search_ = search.value.toLowerCase()
|
||||
// Check if any value in apps (label, id, name, description) match the search query.
|
||||
const filtered = apps.value.filter((app) =>
|
||||
Object.values(app).some(
|
||||
(item) => item && item.toLowerCase().includes(search_),
|
||||
),
|
||||
const apps = ref<Obj[] | undefined>()
|
||||
const [search, filteredApps] = useSearch(apps, (s, app) => {
|
||||
return Object.values(app).some(
|
||||
(value) => value && value.toLowerCase().includes(s),
|
||||
)
|
||||
return filtered.length ? filtered : null
|
||||
})
|
||||
|
||||
function onQueriesResponse(data: any) {
|
||||
if (data.apps.length === 0) {
|
||||
apps.value = null
|
||||
apps.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -39,10 +34,9 @@ function onQueriesResponse(data: any) {
|
|||
|
||||
<template>
|
||||
<ViewSearch
|
||||
v-model:search="search"
|
||||
:filtered-items="filteredApps"
|
||||
v-model="search"
|
||||
items-name="installed_apps"
|
||||
:items="apps"
|
||||
:items="filteredApps"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #top-bar-buttons>
|
||||
|
|
|
@ -1,41 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import { useStoreGetters } from '@/store/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import RecursiveListGroup from '@/components/RecursiveListGroup.vue'
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { useSearch } from '@/composables/useSearch'
|
||||
import type { TreeRootNode } from '@/helpers/data/tree'
|
||||
import type { ComputedRef } from 'vue'
|
||||
|
||||
const { domains, mainDomain, domainsTree } = useStoreGetters()
|
||||
const { mainDomain, domainsTree } = useStoreGetters()
|
||||
const { loading } = useInitialQueries([
|
||||
['GET', { uri: 'domains', storeKey: 'domains' }],
|
||||
])
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const tree = computed(() => {
|
||||
const [search, filteredTree] = useSearch(
|
||||
// FIXME rm ts type when moved to pinia or else
|
||||
const tree = domainsTree.value as TreeRootNode | undefined
|
||||
if (!tree) return
|
||||
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
|
||||
})
|
||||
domainsTree as ComputedRef<TreeRootNode | undefined>,
|
||||
(s, node) => node.id.includes(s),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ViewSearch
|
||||
id="domain-list"
|
||||
v-model:search="search"
|
||||
:filtered-items="hasFilteredItems"
|
||||
:items="domains"
|
||||
v-model="search"
|
||||
:items="filteredTree ? filteredTree.children : filteredTree"
|
||||
items-name="domains"
|
||||
:loading="loading"
|
||||
>
|
||||
|
@ -47,8 +35,8 @@ const hasFilteredItems = computed(() => {
|
|||
</template>
|
||||
|
||||
<RecursiveListGroup
|
||||
v-if="tree"
|
||||
:tree="tree"
|
||||
v-if="filteredTree"
|
||||
:tree="filteredTree"
|
||||
:toggle-text="$t('domain.toggle_subdomains')"
|
||||
class="mb-5"
|
||||
>
|
||||
|
@ -67,9 +55,9 @@ const hasFilteredItems = computed(() => {
|
|||
|
||||
<small
|
||||
v-if="data.name === mainDomain"
|
||||
v-b-tooltip.hover
|
||||
:title="$t('domain.types.main_domain')"
|
||||
class="ms-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<YIcon iname="star" />
|
||||
</small>
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import api from '@/api'
|
||||
import TagsSelectizeItem from '@/components/globals/formItems/TagsSelectizeItem.vue'
|
||||
import { useAutoModal } from '@/composables/useAutoModal'
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { useSearch } from '@/composables/useSearch'
|
||||
import { isEmptyValue } from '@/helpers/commons'
|
||||
import type { Obj } from '@/types/commons'
|
||||
|
||||
// TODO add global search with type (search by: group, user, permission)
|
||||
// TODO add vuex store update on inputs ?
|
||||
|
@ -28,25 +30,15 @@ const { loading } = useInitialQueries(
|
|||
{ onQueriesResponse },
|
||||
)
|
||||
|
||||
const search = ref('')
|
||||
const permissions = ref()
|
||||
const permissionsOptions = ref()
|
||||
const primaryGroups = ref()
|
||||
const primaryGroups = ref<Obj[] | undefined>()
|
||||
const userGroups = ref()
|
||||
const usersOptions = ref()
|
||||
const activeUserGroups = ref()
|
||||
|
||||
const filteredGroups = computed(() => {
|
||||
const groups = primaryGroups.value
|
||||
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
|
||||
const [search, filteredGroups] = useSearch(primaryGroups, (s, group) => {
|
||||
return group.name.toLowerCase().includes(s)
|
||||
})
|
||||
|
||||
function onQueriesResponse(users: any, allGroups: any, permsDict: any) {
|
||||
|
@ -57,12 +49,12 @@ function onQueriesResponse(users: any, allGroups: any, permsDict: any) {
|
|||
...value,
|
||||
}))
|
||||
const userNames = users ? Object.keys(users) : []
|
||||
const primaryGroups_ = {}
|
||||
const primaryGroups_ = []
|
||||
const userGroups_ = {}
|
||||
|
||||
for (const groupName in allGroups) {
|
||||
// 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) => {
|
||||
return permsDict[perm].label
|
||||
})
|
||||
|
@ -103,7 +95,7 @@ function onQueriesResponse(users: any, allGroups: any, permsDict: any) {
|
|||
.map(({ id }) => permsDict[id].label)
|
||||
}
|
||||
|
||||
primaryGroups_[groupName] = group_
|
||||
primaryGroups_.push(group_)
|
||||
}
|
||||
|
||||
const activeUserGroups_ = Object.entries(userGroups_)
|
||||
|
@ -179,16 +171,17 @@ async function deleteGroup(groupName) {
|
|||
{ key: 'groups.delete', name: groupName },
|
||||
)
|
||||
.then(() => {
|
||||
delete primaryGroups.value[groupName]
|
||||
primaryGroups.value = primaryGroups.value?.filter(
|
||||
(group) => group.name !== groupName,
|
||||
)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ViewSearch
|
||||
v-model:search="search"
|
||||
:filtered-items="filteredGroups"
|
||||
:items="primaryGroups"
|
||||
v-model="search"
|
||||
:items="filteredGroups"
|
||||
items-name="groups"
|
||||
:loading="loading"
|
||||
skeleton="CardFormSkeleton"
|
||||
|
@ -201,13 +194,13 @@ async function deleteGroup(groupName) {
|
|||
|
||||
<!-- PRIMARY GROUPS CARDS -->
|
||||
<YCard
|
||||
v-for="(group, groupName) in filteredGroups"
|
||||
:key="groupName"
|
||||
v-for="group in filteredGroups"
|
||||
:key="group.name"
|
||||
collapsable
|
||||
:title="
|
||||
group.isSpecial
|
||||
? $t('group_' + groupName)
|
||||
: `${$t('group')} '${groupName}'`
|
||||
? $t('group_' + group.name)
|
||||
: `${$t('group')} '${group.name}'`
|
||||
"
|
||||
icon="group"
|
||||
>
|
||||
|
@ -215,7 +208,7 @@ async function deleteGroup(groupName) {
|
|||
<!-- DELETE GROUP -->
|
||||
<BButton
|
||||
v-if="!group.isSpecial"
|
||||
@click="deleteGroup(groupName)"
|
||||
@click="deleteGroup(group.name)"
|
||||
size="sm"
|
||||
variant="danger"
|
||||
>
|
||||
|
@ -231,23 +224,23 @@ async function deleteGroup(groupName) {
|
|||
<template v-if="group.isSpecial">
|
||||
<p class="text-primary-emphasis">
|
||||
<YIcon iname="info-circle" />
|
||||
{{ $t('group_explain_' + groupName) }}
|
||||
{{ $t('group_explain_' + group.name) }}
|
||||
</p>
|
||||
<p class="text-primary-emphasis" v-if="groupName === 'visitors'">
|
||||
<p class="text-primary-emphasis" v-if="group.name === 'visitors'">
|
||||
<em>{{
|
||||
$t('group_explain_visitors_needed_for_external_client')
|
||||
}}</em>
|
||||
</p>
|
||||
</template>
|
||||
<template v-if="groupName == 'admins' || !group.isSpecial">
|
||||
<template v-if="group.name == 'admins' || !group.isSpecial">
|
||||
<TagsSelectizeItem
|
||||
v-model="group.members"
|
||||
:options="usersOptions"
|
||||
:id="groupName + '-users'"
|
||||
:id="group.name + '-users'"
|
||||
:label="$t('group_add_member')"
|
||||
tag-icon="user"
|
||||
items-name="users"
|
||||
@tag-update="onUserChanged({ ...$event, groupName })"
|
||||
@tag-update="onUserChanged({ ...$event, groupName: group.name })"
|
||||
/>
|
||||
</template>
|
||||
</BCol>
|
||||
|
@ -262,11 +255,13 @@ async function deleteGroup(groupName) {
|
|||
<TagsSelectizeItem
|
||||
v-model="group.permissions"
|
||||
:options="permissionsOptions"
|
||||
:id="groupName + '-perms'"
|
||||
:id="group.name + '-perms'"
|
||||
:label="$t('group_add_permission')"
|
||||
tag-icon="key-modern"
|
||||
items-name="permissions"
|
||||
@tag-update="onPermissionChanged({ ...$event, groupName })"
|
||||
@tag-update="
|
||||
onPermissionChanged({ ...$event, groupName: group.name })
|
||||
"
|
||||
:disabled-items="group.disabledItems"
|
||||
/>
|
||||
</BCol>
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { useSearch } from '@/composables/useSearch'
|
||||
import { distanceToNow } from '@/helpers/filters/date'
|
||||
import type { Obj } from '@/types/commons'
|
||||
|
||||
const { loading } = useInitialQueries([['GET', 'services']], {
|
||||
onQueriesResponse,
|
||||
})
|
||||
const search = ref('')
|
||||
const services = ref()
|
||||
|
||||
const filteredServices = computed(() => {
|
||||
if (!services.value) return
|
||||
const services_ = services.value.filter(({ name }) => {
|
||||
return name.toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
return services_.length ? services_ : null
|
||||
const services = ref<Obj[] | undefined>()
|
||||
const [search, filteredServices] = useSearch(services, (s, service) => {
|
||||
return service.name.toLowerCase().includes(s)
|
||||
})
|
||||
|
||||
function onQueriesResponse(services_: any) {
|
||||
|
@ -34,9 +31,8 @@ function onQueriesResponse(services_: any) {
|
|||
<template>
|
||||
<ViewSearch
|
||||
id="service-list"
|
||||
v-model:search="search"
|
||||
:filtered-items="filteredServices"
|
||||
:items="services"
|
||||
v-model="search"
|
||||
:items="filteredServices"
|
||||
items-name="services"
|
||||
:loading="loading"
|
||||
>
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { useSearch } from '@/composables/useSearch'
|
||||
import { distanceToNow, readableDate } from '@/helpers/filters/date'
|
||||
import type { Obj } from '@/types/commons'
|
||||
|
||||
const { loading } = useInitialQueries(
|
||||
[['GET', `logs?limit=${25}&with_details`]],
|
||||
{ onQueriesResponse },
|
||||
)
|
||||
|
||||
const search = ref('')
|
||||
const operations = ref()
|
||||
|
||||
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
|
||||
const operations = ref<Obj[] | undefined>()
|
||||
const [search, filteredOperations] = useSearch(operations, (s, op) => {
|
||||
return op.description.toLowerCase().includes(s)
|
||||
})
|
||||
|
||||
function onQueriesResponse({ operation }: any) {
|
||||
|
@ -40,9 +35,8 @@ function onQueriesResponse({ operation }: any) {
|
|||
|
||||
<template>
|
||||
<ViewSearch
|
||||
v-model:search="search"
|
||||
:filtered-items="filteredOperations"
|
||||
:items="operations"
|
||||
v-model="search"
|
||||
:items="filteredOperations"
|
||||
items-name="logs"
|
||||
:loading="loading"
|
||||
skeleton="CardListSkeleton"
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { type ComputedRef } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||
import { useSearch } from '@/composables/useSearch'
|
||||
import { useStoreGetters } from '@/store/utils'
|
||||
import type { Obj } from '@/types/commons'
|
||||
|
||||
const store = useStore()
|
||||
const { loading } = useInitialQueries([
|
||||
|
@ -15,20 +17,13 @@ const { loading } = useInitialQueries([
|
|||
},
|
||||
],
|
||||
])
|
||||
const { users } = useStoreGetters()
|
||||
|
||||
const search = ref('')
|
||||
const filteredUsers = computed(() => {
|
||||
if (!users.value) return
|
||||
const search_ = search.value.toLowerCase()
|
||||
const filtered = users.value.filter((user) => {
|
||||
return (
|
||||
user.username.toLowerCase().includes(search_) ||
|
||||
user.groups.includes(search_)
|
||||
)
|
||||
})
|
||||
return filtered.length === 0 ? null : filtered
|
||||
})
|
||||
const { users } = useStoreGetters()
|
||||
const [search, filteredUsers] = useSearch(
|
||||
users as ComputedRef<Obj[] | undefined>,
|
||||
(s, user) =>
|
||||
user.username.toLowerCase().includes(s) || user.groups.includes(s),
|
||||
)
|
||||
|
||||
function downloadExport() {
|
||||
const host = store.getters.host
|
||||
|
@ -38,9 +33,8 @@ function downloadExport() {
|
|||
|
||||
<template>
|
||||
<ViewSearch
|
||||
v-model:search="search"
|
||||
:filtered-items="filteredUsers"
|
||||
:items="users"
|
||||
v-model="search"
|
||||
:items="filteredUsers"
|
||||
items-name="users"
|
||||
:loading="loading"
|
||||
>
|
||||
|
|
Loading…
Reference in a new issue