mirror of https://github.com/halo-dev/halo
refactor: data list filter components (#4182)
#### What type of PR is this? /area console /kind improvement /milestone 2.8.x #### What this PR does / why we need it: 重构 Console 端数据列表的筛选项 UI,并提供全局的筛选列表组件和搜索输入框组件。 > 在此 PR 同时重构了插件列表和用户列表用于验证,其他页面后续提 PR 修改。 <img width="1657" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/ffa42184-46e6-4cb0-b39f-bd7b18869697"> 重构之后可以获得: 1. 更好的代码组织。 2. 更好的使用体验。 3. UI 更加容易适配移动端。 #### Which issue(s) this PR fixes: Fixes #4181 #### Special notes for your reviewer: 需要测试: 1. 测试插件管理和用户管理的数据列表筛选和关键词搜索功能是否正常。 #### Does this PR introduce a user-facing change? ```release-note 重构 Console 端数据列表的筛选项 UI,并提供全局的筛选列表组件和搜索输入框组件。 ```pull/4195/head
parent
c0aae3a63c
commit
f622b1787c
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { IconArrowDown, VDropdown, VDropdownItem } from "@halo-dev/components";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
items: {
|
||||||
|
label: string;
|
||||||
|
value?: string | boolean | number;
|
||||||
|
}[];
|
||||||
|
label: string;
|
||||||
|
modelValue?: string | boolean | number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
event: "update:modelValue",
|
||||||
|
modelValue: string | boolean | number | undefined
|
||||||
|
): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedItem = computed(() => {
|
||||||
|
return props.items.find((item) => item.value === props.modelValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSelect(item: {
|
||||||
|
label: string;
|
||||||
|
value?: string | boolean | number;
|
||||||
|
}) {
|
||||||
|
if (item.value === props.modelValue) {
|
||||||
|
emit("update:modelValue", undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("update:modelValue", item.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDropdown>
|
||||||
|
<div
|
||||||
|
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||||
|
:class="{ 'font-semibold text-gray-700': modelValue !== undefined }"
|
||||||
|
>
|
||||||
|
<span v-if="!selectedItem" class="mr-0.5">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
<span v-else> {{ label }}:{{ selectedItem.label }} </span>
|
||||||
|
<span>
|
||||||
|
<IconArrowDown />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<template #popper>
|
||||||
|
<VDropdownItem
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:key="index"
|
||||||
|
:selected="item.value === modelValue"
|
||||||
|
@click="handleSelect(item)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</VDropdownItem>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</template>
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { getNode, reset } from "@formkit/core";
|
||||||
|
import { IconCloseCircle } from "@halo-dev/components";
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
placeholder?: string;
|
||||||
|
modelValue: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
placeholder: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:modelValue", modelValue: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const id = `search-input-${crypto.randomUUID()}`;
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
emit("update:modelValue", "");
|
||||||
|
reset(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeywordChange() {
|
||||||
|
const keywordNode = getNode(id);
|
||||||
|
if (keywordNode) {
|
||||||
|
emit("update:modelValue", keywordNode._value as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormKit
|
||||||
|
:id="id"
|
||||||
|
outer-class="!p-0"
|
||||||
|
:placeholder="placeholder || $t('core.common.placeholder.search')"
|
||||||
|
type="text"
|
||||||
|
name="keyword"
|
||||||
|
:model-value="modelValue"
|
||||||
|
@keyup.enter="onKeywordChange"
|
||||||
|
>
|
||||||
|
<template v-if="modelValue" #suffix>
|
||||||
|
<div
|
||||||
|
class="group flex h-full cursor-pointer items-center bg-white px-2 transition-all hover:bg-gray-50"
|
||||||
|
@click="handleReset"
|
||||||
|
>
|
||||||
|
<IconCloseCircle
|
||||||
|
class="h-4 w-4 text-gray-500 group-hover:text-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FormKit>
|
||||||
|
</template>
|
|
@ -748,7 +748,6 @@ core:
|
||||||
filters:
|
filters:
|
||||||
status:
|
status:
|
||||||
items:
|
items:
|
||||||
all: All
|
|
||||||
active: Active
|
active: Active
|
||||||
inactive: Inactive
|
inactive: Inactive
|
||||||
sort:
|
sort:
|
||||||
|
@ -1190,6 +1189,9 @@ core:
|
||||||
labels:
|
labels:
|
||||||
sort: Sort
|
sort: Sort
|
||||||
status: Status
|
status: Status
|
||||||
|
item_labels:
|
||||||
|
all: All
|
||||||
|
default: Default
|
||||||
status:
|
status:
|
||||||
deleting: Deleting
|
deleting: Deleting
|
||||||
loading: Loading
|
loading: Loading
|
||||||
|
|
|
@ -748,7 +748,6 @@ core:
|
||||||
filters:
|
filters:
|
||||||
status:
|
status:
|
||||||
items:
|
items:
|
||||||
all: 全部
|
|
||||||
active: 已启用
|
active: 已启用
|
||||||
inactive: 未启用
|
inactive: 未启用
|
||||||
sort:
|
sort:
|
||||||
|
@ -1190,6 +1189,9 @@ core:
|
||||||
labels:
|
labels:
|
||||||
sort: 排序
|
sort: 排序
|
||||||
status: 状态
|
status: 状态
|
||||||
|
item_labels:
|
||||||
|
all: 全部
|
||||||
|
default: 默认
|
||||||
status:
|
status:
|
||||||
deleting: 删除中
|
deleting: 删除中
|
||||||
loading: 加载中
|
loading: 加载中
|
||||||
|
|
|
@ -748,7 +748,6 @@ core:
|
||||||
filters:
|
filters:
|
||||||
status:
|
status:
|
||||||
items:
|
items:
|
||||||
all: 全部
|
|
||||||
active: 已啟用
|
active: 已啟用
|
||||||
inactive: 未啟用
|
inactive: 未啟用
|
||||||
sort:
|
sort:
|
||||||
|
@ -1190,6 +1189,9 @@ core:
|
||||||
labels:
|
labels:
|
||||||
sort: 排序
|
sort: 排序
|
||||||
status: 狀態
|
status: 狀態
|
||||||
|
item_labels:
|
||||||
|
all: 全部
|
||||||
|
default: 預設
|
||||||
status:
|
status:
|
||||||
deleting: 刪除中
|
deleting: 刪除中
|
||||||
loading: 加載中
|
loading: 加載中
|
||||||
|
|
|
@ -44,7 +44,6 @@ import { useRouteQuery } from "@vueuse/router";
|
||||||
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
|
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
|
||||||
import { getNode } from "@formkit/core";
|
import { getNode } from "@formkit/core";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ import type { ListedComment, User } from "@halo-dev/api-client";
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
|
||||||
import { getNode } from "@formkit/core";
|
import { getNode } from "@formkit/core";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
|
@ -37,7 +37,6 @@ import cloneDeep from "lodash.clonedeep";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { singlePageLabels } from "@/constants/labels";
|
import { singlePageLabels } from "@/constants/labels";
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
|
||||||
import { getNode } from "@formkit/core";
|
import { getNode } from "@formkit/core";
|
||||||
import { useMutation, useQuery } from "@tanstack/vue-query";
|
import { useMutation, useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
|
@ -43,7 +43,6 @@ import { formatDatetime } from "@/utils/date";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { postLabels } from "@/constants/labels";
|
import { postLabels } from "@/constants/labels";
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
|
||||||
import { getNode } from "@formkit/core";
|
import { getNode } from "@formkit/core";
|
||||||
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
|
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
|
||||||
import { useMutation, useQuery } from "@tanstack/vue-query";
|
import { useMutation, useQuery } from "@tanstack/vue-query";
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
IconAddCircle,
|
IconAddCircle,
|
||||||
IconArrowDown,
|
|
||||||
IconPlug,
|
IconPlug,
|
||||||
IconRefreshLine,
|
IconRefreshLine,
|
||||||
VButton,
|
VButton,
|
||||||
|
@ -11,8 +10,6 @@ import {
|
||||||
VPagination,
|
VPagination,
|
||||||
VSpace,
|
VSpace,
|
||||||
VLoading,
|
VLoading,
|
||||||
VDropdown,
|
|
||||||
VDropdownItem,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import PluginListItem from "./components/PluginListItem.vue";
|
import PluginListItem from "./components/PluginListItem.vue";
|
||||||
|
@ -20,26 +17,14 @@ import PluginUploadModal from "./components/PluginUploadModal.vue";
|
||||||
import { computed, ref, onMounted } from "vue";
|
import { computed, ref, onMounted } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
|
||||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
|
||||||
import { getNode } from "@formkit/core";
|
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import type { Plugin } from "@halo-dev/api-client";
|
import type { Plugin } from "@halo-dev/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import { watch } from "vue";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
interface EnabledItem {
|
|
||||||
label: string;
|
|
||||||
value?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SortItem {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
const pluginInstall = ref(false);
|
const pluginInstall = ref(false);
|
||||||
|
@ -49,90 +34,41 @@ const page = ref(1);
|
||||||
const size = ref(20);
|
const size = ref(20);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
|
|
||||||
// Filters
|
const selectedEnabledValue = ref();
|
||||||
const EnabledItems: EnabledItem[] = [
|
const selectedSortValue = ref();
|
||||||
{
|
|
||||||
label: t("core.plugin.filters.status.items.all"),
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("core.plugin.filters.status.items.active"),
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("core.plugin.filters.status.items.inactive"),
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const SortItems: SortItem[] = [
|
|
||||||
{
|
|
||||||
label: t("core.plugin.filters.sort.items.create_time_desc"),
|
|
||||||
value: "creationTimestamp,desc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("core.plugin.filters.sort.items.create_time_asc"),
|
|
||||||
value: "creationTimestamp,asc",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectedEnabledItem = ref<EnabledItem>();
|
|
||||||
const selectedSortItem = ref<SortItem>();
|
|
||||||
|
|
||||||
function handleEnabledItemChange(enabledItem: EnabledItem) {
|
|
||||||
selectedEnabledItem.value = enabledItem;
|
|
||||||
page.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSortItemChange(sortItem?: SortItem) {
|
|
||||||
selectedSortItem.value = sortItem;
|
|
||||||
page.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeywordChange() {
|
|
||||||
const keywordNode = getNode("keywordInput");
|
|
||||||
if (keywordNode) {
|
|
||||||
keyword.value = keywordNode._value as string;
|
|
||||||
}
|
|
||||||
page.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClearKeyword() {
|
|
||||||
keyword.value = "";
|
|
||||||
page.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasFilters = computed(() => {
|
const hasFilters = computed(() => {
|
||||||
return (
|
return selectedEnabledValue.value !== undefined || selectedSortValue.value;
|
||||||
selectedEnabledItem.value?.value !== undefined ||
|
|
||||||
selectedSortItem.value?.value ||
|
|
||||||
keyword.value
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClearFilters() {
|
function handleClearFilters() {
|
||||||
selectedEnabledItem.value = undefined;
|
selectedSortValue.value = undefined;
|
||||||
selectedSortItem.value = undefined;
|
selectedEnabledValue.value = undefined;
|
||||||
keyword.value = "";
|
|
||||||
page.value = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [selectedEnabledValue.value, selectedSortValue.value, keyword.value],
|
||||||
|
() => {
|
||||||
|
page.value = 1;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
"plugins",
|
"plugins",
|
||||||
page,
|
page,
|
||||||
size,
|
size,
|
||||||
keyword,
|
keyword,
|
||||||
selectedEnabledItem,
|
selectedEnabledValue,
|
||||||
selectedSortItem,
|
selectedSortValue,
|
||||||
],
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiClient.plugin.listPlugins({
|
const { data } = await apiClient.plugin.listPlugins({
|
||||||
page: page.value,
|
page: page.value,
|
||||||
size: size.value,
|
size: size.value,
|
||||||
keyword: keyword.value,
|
keyword: keyword.value,
|
||||||
enabled: selectedEnabledItem.value?.value,
|
enabled: selectedEnabledValue.value,
|
||||||
sort: [selectedSortItem.value?.value].filter(Boolean) as string[],
|
sort: [selectedSortValue.value].filter(Boolean) as string[],
|
||||||
});
|
});
|
||||||
|
|
||||||
total.value = data.total;
|
total.value = data.total;
|
||||||
|
@ -204,99 +140,52 @@ onMounted(() => {
|
||||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||||
>
|
>
|
||||||
<div class="flex w-full flex-1 items-center gap-2 sm:w-auto">
|
<div class="flex w-full flex-1 items-center gap-2 sm:w-auto">
|
||||||
<FormKit
|
<SearchInput v-model="keyword" />
|
||||||
id="keywordInput"
|
</div>
|
||||||
outer-class="!p-0"
|
<div class="mt-4 flex sm:mt-0">
|
||||||
:placeholder="$t('core.common.placeholder.search')"
|
<VSpace spacing="lg">
|
||||||
type="text"
|
|
||||||
name="keyword"
|
|
||||||
:model-value="keyword"
|
|
||||||
@keyup.enter="handleKeywordChange"
|
|
||||||
></FormKit>
|
|
||||||
|
|
||||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
|
||||||
{{
|
|
||||||
$t("core.common.filters.results.keyword", {
|
|
||||||
keyword: keyword,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</FilterTag>
|
|
||||||
|
|
||||||
<FilterTag
|
|
||||||
v-if="selectedEnabledItem?.value !== undefined"
|
|
||||||
@close="handleEnabledItemChange(EnabledItems[0])"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
$t("core.common.filters.results.status", {
|
|
||||||
status: selectedEnabledItem.label,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</FilterTag>
|
|
||||||
|
|
||||||
<FilterTag
|
|
||||||
v-if="selectedSortItem"
|
|
||||||
@close="handleSortItemChange()"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
$t("core.common.filters.results.sort", {
|
|
||||||
sort: selectedSortItem.label,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</FilterTag>
|
|
||||||
|
|
||||||
<FilterCleanButton
|
<FilterCleanButton
|
||||||
v-if="hasFilters"
|
v-if="hasFilters"
|
||||||
@click="handleClearFilters"
|
@click="handleClearFilters"
|
||||||
/>
|
/>
|
||||||
</div>
|
<FilterDropdown
|
||||||
<div class="mt-4 flex sm:mt-0">
|
v-model="selectedEnabledValue"
|
||||||
<VSpace spacing="lg">
|
:label="$t('core.common.filters.labels.status')"
|
||||||
<VDropdown>
|
:items="[
|
||||||
<div
|
{
|
||||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
label: t('core.common.filters.item_labels.all'),
|
||||||
>
|
},
|
||||||
<span class="mr-0.5">
|
{
|
||||||
{{ $t("core.common.filters.labels.status") }}
|
label: t('core.plugin.filters.status.items.active'),
|
||||||
</span>
|
value: true,
|
||||||
<span>
|
},
|
||||||
<IconArrowDown />
|
{
|
||||||
</span>
|
label: t('core.plugin.filters.status.items.inactive'),
|
||||||
</div>
|
value: false,
|
||||||
<template #popper>
|
},
|
||||||
<VDropdownItem
|
]"
|
||||||
v-for="(enabledItem, index) in EnabledItems"
|
/>
|
||||||
:key="index"
|
<FilterDropdown
|
||||||
:selected="
|
v-model="selectedSortValue"
|
||||||
enabledItem.value === selectedEnabledItem?.value
|
:label="$t('core.common.filters.labels.sort')"
|
||||||
"
|
:items="[
|
||||||
@click="handleEnabledItemChange(enabledItem)"
|
{
|
||||||
>
|
label: t('core.common.filters.item_labels.default'),
|
||||||
{{ enabledItem.label }}
|
},
|
||||||
</VDropdownItem>
|
{
|
||||||
</template>
|
label: t(
|
||||||
</VDropdown>
|
'core.plugin.filters.sort.items.create_time_desc'
|
||||||
<VDropdown>
|
),
|
||||||
<div
|
value: 'creationTimestamp,desc',
|
||||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
},
|
||||||
>
|
{
|
||||||
<span class="mr-0.5">
|
label: t(
|
||||||
{{ $t("core.common.filters.labels.sort") }}
|
'core.plugin.filters.sort.items.create_time_asc'
|
||||||
</span>
|
),
|
||||||
<span>
|
value: 'creationTimestamp,asc',
|
||||||
<IconArrowDown />
|
},
|
||||||
</span>
|
]"
|
||||||
</div>
|
/>
|
||||||
<template #popper>
|
|
||||||
<VDropdownItem
|
|
||||||
v-for="(sortItem, index) in SortItems"
|
|
||||||
:key="index"
|
|
||||||
:selected="sortItem.value === selectedSortItem?.value"
|
|
||||||
@click="handleSortItemChange(sortItem)"
|
|
||||||
>
|
|
||||||
{{ sortItem.label }}
|
|
||||||
</VDropdownItem>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div
|
<div
|
||||||
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
IconAddCircle,
|
IconAddCircle,
|
||||||
IconArrowDown,
|
|
||||||
IconUserFollow,
|
IconUserFollow,
|
||||||
IconUserSettings,
|
IconUserSettings,
|
||||||
IconLockPasswordLine,
|
IconLockPasswordLine,
|
||||||
|
@ -20,7 +19,6 @@ import {
|
||||||
Toast,
|
Toast,
|
||||||
IconRefreshLine,
|
IconRefreshLine,
|
||||||
VEmpty,
|
VEmpty,
|
||||||
VDropdown,
|
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import UserEditingModal from "./components/UserEditingModal.vue";
|
import UserEditingModal from "./components/UserEditingModal.vue";
|
||||||
|
@ -28,16 +26,13 @@ import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
|
||||||
import GrantPermissionModal from "./components/GrantPermissionModal.vue";
|
import GrantPermissionModal from "./components/GrantPermissionModal.vue";
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import type { Role, User, ListedUser } from "@halo-dev/api-client";
|
import type { User, ListedUser } from "@halo-dev/api-client";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { getNode } from "@formkit/core";
|
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
|
||||||
import { useFetchRole } from "../roles/composables/use-role";
|
import { useFetchRole } from "../roles/composables/use-role";
|
||||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import UserCreationModal from "./components/UserCreationModal.vue";
|
import UserCreationModal from "./components/UserCreationModal.vue";
|
||||||
|
@ -61,61 +56,26 @@ const ANONYMOUSUSER_NAME = "anonymousUser";
|
||||||
const DELETEDUSER_NAME = "ghost";
|
const DELETEDUSER_NAME = "ghost";
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
function handleKeywordChange() {
|
|
||||||
const keywordNode = getNode("keywordInput");
|
|
||||||
if (keywordNode) {
|
|
||||||
keyword.value = keywordNode._value as string;
|
|
||||||
}
|
|
||||||
page.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClearKeyword() {
|
|
||||||
keyword.value = "";
|
|
||||||
page.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SortItem {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SortItems: SortItem[] = [
|
|
||||||
{
|
|
||||||
label: t("core.user.filters.sort.items.create_time_desc"),
|
|
||||||
value: "creationTimestamp,desc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("core.user.filters.sort.items.create_time_asc"),
|
|
||||||
value: "creationTimestamp,asc",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectedSortItem = ref<SortItem>();
|
|
||||||
|
|
||||||
function handleSortItemChange(sortItem?: SortItem) {
|
|
||||||
selectedSortItem.value = sortItem;
|
|
||||||
page.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { roles } = useFetchRole();
|
const { roles } = useFetchRole();
|
||||||
const selectedRole = ref<Role>();
|
const selectedRoleValue = ref();
|
||||||
|
const selectedSortValue = ref();
|
||||||
function handleRoleChange(role?: Role) {
|
|
||||||
selectedRole.value = role;
|
|
||||||
page.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClearFilters() {
|
function handleClearFilters() {
|
||||||
selectedRole.value = undefined;
|
selectedRoleValue.value = undefined;
|
||||||
selectedSortItem.value = undefined;
|
selectedSortValue.value = undefined;
|
||||||
keyword.value = "";
|
|
||||||
page.value = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFilters = computed(() => {
|
const hasFilters = computed(() => {
|
||||||
return selectedRole.value || selectedSortItem.value || keyword.value;
|
return selectedRoleValue.value || selectedSortValue.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [selectedRoleValue.value, selectedSortValue.value, keyword.value],
|
||||||
|
() => {
|
||||||
|
page.value = 1;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const size = ref(20);
|
const size = ref(20);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
|
@ -126,7 +86,14 @@ const {
|
||||||
isFetching,
|
isFetching,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery<ListedUser[]>({
|
} = useQuery<ListedUser[]>({
|
||||||
queryKey: ["users", page, size, keyword, selectedSortItem, selectedRole],
|
queryKey: [
|
||||||
|
"users",
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
keyword,
|
||||||
|
selectedSortValue,
|
||||||
|
selectedRoleValue,
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiClient.user.listUsers({
|
const { data } = await apiClient.user.listUsers({
|
||||||
page: page.value,
|
page: page.value,
|
||||||
|
@ -136,10 +103,8 @@ const {
|
||||||
`name!=${ANONYMOUSUSER_NAME}`,
|
`name!=${ANONYMOUSUSER_NAME}`,
|
||||||
`name!=${DELETEDUSER_NAME}`,
|
`name!=${DELETEDUSER_NAME}`,
|
||||||
],
|
],
|
||||||
sort: [selectedSortItem.value?.value].filter(
|
sort: [selectedSortValue.value].filter(Boolean) as string[],
|
||||||
(item) => !!item
|
role: selectedRoleValue.value,
|
||||||
) as string[],
|
|
||||||
role: selectedRole.value?.metadata.name,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
total.value = data.total;
|
total.value = data.total;
|
||||||
|
@ -330,55 +295,7 @@ onMounted(() => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-1 items-center sm:w-auto">
|
<div class="flex w-full flex-1 items-center sm:w-auto">
|
||||||
<div
|
<SearchInput v-if="!selectedUserNames.length" v-model="keyword" />
|
||||||
v-if="!selectedUserNames.length"
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FormKit
|
|
||||||
id="keywordInput"
|
|
||||||
outer-class="!p-0"
|
|
||||||
:model-value="keyword"
|
|
||||||
name="keyword"
|
|
||||||
:placeholder="$t('core.common.placeholder.search')"
|
|
||||||
type="text"
|
|
||||||
@keyup.enter="handleKeywordChange"
|
|
||||||
></FormKit>
|
|
||||||
|
|
||||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
|
||||||
{{
|
|
||||||
$t("core.common.filters.results.keyword", {
|
|
||||||
keyword: keyword,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</FilterTag>
|
|
||||||
|
|
||||||
<FilterTag v-if="selectedRole" @close="handleRoleChange()">
|
|
||||||
{{
|
|
||||||
$t("core.user.filters.role.result", {
|
|
||||||
role:
|
|
||||||
selectedRole.metadata.annotations?.[
|
|
||||||
rbacAnnotations.DISPLAY_NAME
|
|
||||||
] || selectedRole.metadata.name,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</FilterTag>
|
|
||||||
|
|
||||||
<FilterTag
|
|
||||||
v-if="selectedSortItem"
|
|
||||||
@close="handleSortItemChange()"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
$t("core.common.filters.results.sort", {
|
|
||||||
sort: selectedSortItem.label,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</FilterTag>
|
|
||||||
|
|
||||||
<FilterCleanButton
|
|
||||||
v-if="hasFilters"
|
|
||||||
@click="handleClearFilters"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<VSpace v-else>
|
<VSpace v-else>
|
||||||
<VButton type="danger" @click="handleDeleteInBatch">
|
<VButton type="danger" @click="handleDeleteInBatch">
|
||||||
{{ $t("core.common.buttons.delete") }}
|
{{ $t("core.common.buttons.delete") }}
|
||||||
|
@ -387,56 +304,45 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex sm:mt-0">
|
<div class="mt-4 flex sm:mt-0">
|
||||||
<VSpace spacing="lg">
|
<VSpace spacing="lg">
|
||||||
<VDropdown>
|
<FilterCleanButton
|
||||||
<div
|
v-if="hasFilters"
|
||||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
@click="handleClearFilters"
|
||||||
>
|
/>
|
||||||
<span class="mr-0.5">
|
<FilterDropdown
|
||||||
{{ $t("core.user.filters.role.label") }}
|
v-model="selectedRoleValue"
|
||||||
</span>
|
:label="$t('core.user.filters.role.label')"
|
||||||
<span>
|
:items="[
|
||||||
<IconArrowDown />
|
{
|
||||||
</span>
|
label: t('core.common.filters.item_labels.all'),
|
||||||
</div>
|
},
|
||||||
<template #popper>
|
...roles.map((role) => {
|
||||||
<VDropdownItem
|
return {
|
||||||
v-for="(role, index) in roles"
|
label:
|
||||||
:key="index"
|
|
||||||
:selected="
|
|
||||||
selectedRole?.metadata.name === role.metadata.name
|
|
||||||
"
|
|
||||||
@click="handleRoleChange(role)"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
role.metadata.annotations?.[
|
role.metadata.annotations?.[
|
||||||
rbacAnnotations.DISPLAY_NAME
|
rbacAnnotations.DISPLAY_NAME
|
||||||
] || role.metadata.name
|
] || role.metadata.name,
|
||||||
}}
|
value: role.metadata.name,
|
||||||
</VDropdownItem>
|
};
|
||||||
</template>
|
}),
|
||||||
</VDropdown>
|
]"
|
||||||
<VDropdown>
|
/>
|
||||||
<div
|
<FilterDropdown
|
||||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
v-model="selectedSortValue"
|
||||||
>
|
:label="$t('core.common.filters.labels.sort')"
|
||||||
<span class="mr-0.5">
|
:items="[
|
||||||
{{ $t("core.common.filters.labels.sort") }}
|
{
|
||||||
</span>
|
label: t('core.common.filters.item_labels.default'),
|
||||||
<span>
|
},
|
||||||
<IconArrowDown />
|
{
|
||||||
</span>
|
label: t('core.user.filters.sort.items.create_time_desc'),
|
||||||
</div>
|
value: 'creationTimestamp,desc',
|
||||||
<template #popper>
|
},
|
||||||
<VDropdownItem
|
{
|
||||||
v-for="(sortItem, index) in SortItems"
|
label: t('core.user.filters.sort.items.create_time_asc'),
|
||||||
:key="index"
|
value: 'creationTimestamp,asc',
|
||||||
:selected="selectedSortItem?.value === sortItem.value"
|
},
|
||||||
@click="handleSortItemChange(sortItem)"
|
]"
|
||||||
>
|
/>
|
||||||
{{ sortItem.label }}
|
|
||||||
</VDropdownItem>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div
|
<div
|
||||||
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
||||||
|
|
|
@ -6,6 +6,9 @@ import "floating-vue/dist/style.css";
|
||||||
import VueGridLayout from "vue-grid-layout";
|
import VueGridLayout from "vue-grid-layout";
|
||||||
import { defaultConfig, plugin as FormKit } from "@formkit/vue";
|
import { defaultConfig, plugin as FormKit } from "@formkit/vue";
|
||||||
import FormKitConfig from "@/formkit/formkit.config";
|
import FormKitConfig from "@/formkit/formkit.config";
|
||||||
|
import FilterDropdown from "@/components/filter/FilterDropdown.vue";
|
||||||
|
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||||
|
import SearchInput from "@/components/input/SearchInput.vue";
|
||||||
|
|
||||||
export function setupComponents(app: App) {
|
export function setupComponents(app: App) {
|
||||||
app.use(VueGridLayout);
|
app.use(VueGridLayout);
|
||||||
|
@ -25,4 +28,9 @@ export function setupComponents(app: App) {
|
||||||
"VCodemirror",
|
"VCodemirror",
|
||||||
defineAsyncComponent(() => import("@/components/codemirror/Codemirror.vue"))
|
defineAsyncComponent(() => import("@/components/codemirror/Codemirror.vue"))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Console components
|
||||||
|
app.component("FilterDropdown", FilterDropdown);
|
||||||
|
app.component("FilterCleanButton", FilterCleanButton);
|
||||||
|
app.component("SearchInput", SearchInput);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue