refactor: logic of post and singlepage data filtering (#4193)

#### What type of PR is this?

/area console
/kind improvement
/milestone 2.8.x

#### What this PR does / why we need it:

重构文章和页面数据管理的筛选条件逻辑以及 UI。

<img width="1646" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/154c5588-91c0-448d-aa39-d75e90bce6ca">


Ref https://github.com/halo-dev/halo/pull/4182
Ref https://github.com/halo-dev/halo/issues/4181

#### Special notes for your reviewer:

需要测试:

1. 测试文章的筛选条件包括关键词筛选功能是否正常。
2. 测试页面的筛选条件包括关键词筛选功能是否正常。

#### Does this PR introduce a user-facing change?

```release-note
重构 Console 端文章数据列表的筛选项 UI 和逻辑。
```
pull/4195/head^2
Ryan Wang 2023-07-11 15:15:10 +08:00 committed by GitHub
parent f622b1787c
commit 119f352145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 357 additions and 838 deletions

View File

@ -1,6 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Category } from "@halo-dev/api-client"; import type { Category } from "@halo-dev/api-client";
import { VEntity, VEntityField, VDropdown } from "@halo-dev/components"; import {
VEntity,
VEntityField,
VDropdown,
IconArrowDown,
} from "@halo-dev/components";
import { setFocus } from "@/formkit/utils/focus"; import { setFocus } from "@/formkit/utils/focus";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
@ -8,16 +13,16 @@ import { usePostCategory } from "@/modules/contents/posts/categories/composables
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
selected?: Category; label: string;
modelValue?: string;
}>(), }>(),
{ {
selected: undefined, modelValue: undefined,
} }
); );
const emit = defineEmits<{ const emit = defineEmits<{
(event: "update:selected", category?: Category): void; (event: "update:modelValue", value?: string): void;
(event: "select", category?: Category): void;
}>(); }>();
const { categories } = usePostCategory(); const { categories } = usePostCategory();
@ -25,24 +30,18 @@ const { categories } = usePostCategory();
const dropdown = ref(); const dropdown = ref();
const handleSelect = (category: Category) => { const handleSelect = (category: Category) => {
if ( if (category.metadata.name === props.modelValue) {
props.selected && emit("update:modelValue", undefined);
category.metadata.name === props.selected.metadata.name } else {
) { emit("update:modelValue", category.metadata.name);
emit("update:selected", undefined);
emit("select", undefined);
return;
} }
emit("update:selected", category);
emit("select", category);
dropdown.value.hide(); dropdown.value.hide();
}; };
function onDropdownShow() { function onDropdownShow() {
setTimeout(() => { setTimeout(() => {
setFocus("categoryDropdownSelectorInput"); setFocus("categoryFilterDropdownInput");
}, 200); }, 200);
} }
@ -72,16 +71,35 @@ const searchResults = computed(() => {
return fuse?.search(keyword.value).map((item) => item.item); return fuse?.search(keyword.value).map((item) => item.item);
}); });
const selectedCategory = computed(() => {
return categories.value?.find(
(category) => category.metadata.name === props.modelValue
);
});
</script> </script>
<template> <template>
<VDropdown ref="dropdown" :classes="['!p-0']" @show="onDropdownShow"> <VDropdown ref="dropdown" :classes="['!p-0']" @show="onDropdownShow">
<slot /> <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="!selectedCategory" class="mr-0.5">
{{ label }}
</span>
<span v-else class="mr-0.5">
{{ label }}{{ selectedCategory.spec.displayName }}
</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper> <template #popper>
<div class="h-96 w-80"> <div class="h-96 w-80">
<div class="border-b border-b-gray-100 bg-white p-4"> <div class="border-b border-b-gray-100 bg-white p-4">
<FormKit <FormKit
id="categoryDropdownSelectorInput" id="categoryFilterDropdownInput"
v-model="keyword" v-model="keyword"
:placeholder="$t('core.common.placeholder.search')" :placeholder="$t('core.common.placeholder.search')"
type="text" type="text"
@ -97,11 +115,7 @@ const searchResults = computed(() => {
:key="index" :key="index"
@click="handleSelect(category)" @click="handleSelect(category)"
> >
<VEntity <VEntity :is-selected="modelValue === category.metadata.name">
:is-selected="
selected?.metadata.name === category.metadata.name
"
>
<template #start> <template #start>
<VEntityField <VEntityField
:title="category.spec.displayName" :title="category.spec.displayName"

View File

@ -48,7 +48,7 @@ function handleSelect(item: {
<span v-if="!selectedItem" class="mr-0.5"> <span v-if="!selectedItem" class="mr-0.5">
{{ label }} {{ label }}
</span> </span>
<span v-else> {{ label }}{{ selectedItem.label }} </span> <span v-else class="mr-0.5"> {{ label }}{{ selectedItem.label }} </span>
<span> <span>
<IconArrowDown /> <IconArrowDown />
</span> </span>

View File

@ -1,6 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Tag } from "@halo-dev/api-client"; import type { Tag } from "@halo-dev/api-client";
import { VEntity, VEntityField, VDropdown } from "@halo-dev/components"; import {
VEntity,
VEntityField,
VDropdown,
IconArrowDown,
} from "@halo-dev/components";
import { setFocus } from "@/formkit/utils/focus"; import { setFocus } from "@/formkit/utils/focus";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
@ -9,16 +14,16 @@ import PostTag from "@/modules/contents/posts/tags/components/PostTag.vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
selected?: Tag; label: string;
modelValue?: string;
}>(), }>(),
{ {
selected: undefined, modelValue: undefined,
} }
); );
const emit = defineEmits<{ const emit = defineEmits<{
(event: "update:selected", tag?: Tag): void; (event: "update:modelValue", value?: string): void;
(event: "select", tag?: Tag): void;
}>(); }>();
const { tags } = usePostTag(); const { tags } = usePostTag();
@ -26,21 +31,18 @@ const { tags } = usePostTag();
const dropdown = ref(); const dropdown = ref();
const handleSelect = (tag: Tag) => { const handleSelect = (tag: Tag) => {
if (props.selected && tag.metadata.name === props.selected.metadata.name) { if (tag.metadata.name === props.modelValue) {
emit("update:selected", undefined); emit("update:modelValue", undefined);
emit("select", undefined); } else {
return; emit("update:modelValue", tag.metadata.name);
} }
emit("update:selected", tag);
emit("select", tag);
dropdown.value.hide(); dropdown.value.hide();
}; };
function onDropdownShow() { function onDropdownShow() {
setTimeout(() => { setTimeout(() => {
setFocus("tagDropdownSelectorInput"); setFocus("tagFilterDropdownInput");
}, 200); }, 200);
} }
@ -70,16 +72,33 @@ const searchResults = computed(() => {
return fuse?.search(keyword.value).map((item) => item.item); return fuse?.search(keyword.value).map((item) => item.item);
}); });
const selectedTag = computed(() => {
return tags.value?.find((tag) => tag.metadata.name === props.modelValue);
});
</script> </script>
<template> <template>
<VDropdown ref="dropdown" :classes="['!p-0']" @show="onDropdownShow"> <VDropdown ref="dropdown" :classes="['!p-0']" @show="onDropdownShow">
<slot /> <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="!selectedTag" class="mr-0.5">
{{ label }}
</span>
<span v-else class="mr-0.5">
{{ label }}{{ selectedTag.spec.displayName }}
</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper> <template #popper>
<div class="h-96 w-80"> <div class="h-96 w-80">
<div class="border-b border-b-gray-100 bg-white p-4"> <div class="border-b border-b-gray-100 bg-white p-4">
<FormKit <FormKit
id="tagDropdownSelectorInput" id="tagFilterDropdownInput"
v-model="keyword" v-model="keyword"
:placeholder="$t('core.common.placeholder.search')" :placeholder="$t('core.common.placeholder.search')"
type="text" type="text"
@ -95,9 +114,7 @@ const searchResults = computed(() => {
:key="index" :key="index"
@click="handleSelect(tag)" @click="handleSelect(tag)"
> >
<VEntity <VEntity :is-selected="modelValue === tag.metadata.name">
:is-selected="selected?.metadata.name === tag.metadata.name"
>
<template #start> <template #start>
<VEntityField :description="tag.status?.permalink"> <VEntityField :description="tag.status?.permalink">
<template #title> <template #title>

View File

@ -2,6 +2,7 @@
import type { User } from "@halo-dev/api-client"; import type { User } from "@halo-dev/api-client";
import { useUserFetch } from "@/modules/system/users/composables/use-user"; import { useUserFetch } from "@/modules/system/users/composables/use-user";
import { import {
IconArrowDown,
VAvatar, VAvatar,
VDropdown, VDropdown,
VEntity, VEntity,
@ -13,16 +14,16 @@ import Fuse from "fuse.js";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
selected?: User; label: string;
modelValue?: string;
}>(), }>(),
{ {
selected: undefined, modelValue: undefined,
} }
); );
const emit = defineEmits<{ const emit = defineEmits<{
(event: "update:selected", user?: User): void; (event: "update:modelValue", value?: string): void;
(event: "select", user?: User): void;
}>(); }>();
const { users, handleFetchUsers } = useUserFetch(); const { users, handleFetchUsers } = useUserFetch();
@ -30,22 +31,19 @@ const { users, handleFetchUsers } = useUserFetch();
const dropdown = ref(); const dropdown = ref();
const handleSelect = (user: User) => { const handleSelect = (user: User) => {
if (props.selected && user.metadata.name === props.selected.metadata.name) { if (user.metadata.name === props.modelValue) {
emit("update:selected", undefined); emit("update:modelValue", undefined);
emit("select", undefined); } else {
return; emit("update:modelValue", user.metadata.name);
} }
emit("update:selected", user);
emit("select", user);
dropdown.value.hide(); dropdown.value.hide();
}; };
function onDropdownShow() { function onDropdownShow() {
handleFetchUsers(); handleFetchUsers();
setTimeout(() => { setTimeout(() => {
setFocus("userDropdownSelectorInput"); setFocus("userFilterDropdownInput");
}, 200); }, 200);
} }
@ -72,16 +70,33 @@ const searchResults = computed(() => {
return fuse?.search(keyword.value).map((item) => item.item); return fuse?.search(keyword.value).map((item) => item.item);
}); });
const selectedUser = computed(() => {
return users.value.find((user) => user.metadata.name === props.modelValue);
});
</script> </script>
<template> <template>
<VDropdown ref="dropdown" :classes="['!p-0']" @show="onDropdownShow"> <VDropdown ref="dropdown" :classes="['!p-0']" @show="onDropdownShow">
<slot /> <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="!selectedUser" class="mr-0.5">
{{ label }}
</span>
<span v-else class="mr-0.5">
{{ label }}{{ selectedUser.spec.displayName }}
</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper> <template #popper>
<div class="h-96 w-80"> <div class="h-96 w-80">
<div class="border-b border-b-gray-100 bg-white p-4"> <div class="border-b border-b-gray-100 bg-white p-4">
<FormKit <FormKit
id="userDropdownSelectorInput" id="userFilterDropdownInput"
v-model="keyword" v-model="keyword"
:placeholder="$t('core.common.placeholder.search')" :placeholder="$t('core.common.placeholder.search')"
type="text" type="text"
@ -97,9 +112,7 @@ const searchResults = computed(() => {
:key="index" :key="index"
@click="handleSelect(user)" @click="handleSelect(user)"
> >
<VEntity <VEntity :is-selected="modelValue === user.metadata.name">
:is-selected="selected?.metadata.name === user.metadata.name"
>
<template #start> <template #start>
<VEntityField> <VEntityField>
<template #description> <template #description>

View File

@ -154,14 +154,12 @@ core:
filters: filters:
status: status:
items: items:
all: All
published: Published published: Published
draft: Draft draft: Draft
visible: visible:
label: Visible label: Visible
result: "Visible: {visible}" result: "Visible: {visible}"
items: items:
all: All
public: Public public: Public
private: Private private: Private
category: category:
@ -324,14 +322,12 @@ core:
filters: filters:
status: status:
items: items:
all: All
published: Published published: Published
draft: Draft draft: Draft
visible: visible:
label: Visible label: Visible
result: "Visible: {visible}" result: "Visible: {visible}"
items: items:
all: All
public: Public public: Public
private: Private private: Private
author: author:

View File

@ -154,14 +154,12 @@ core:
filters: filters:
status: status:
items: items:
all: 全部
published: 已发布 published: 已发布
draft: 未发布 draft: 未发布
visible: visible:
label: 可见性 label: 可见性
result: "可见性:{visible}" result: "可见性:{visible}"
items: items:
all: 全部
public: 公开 public: 公开
private: 私有 private: 私有
category: category:
@ -324,14 +322,12 @@ core:
filters: filters:
status: status:
items: items:
all: 全部
published: 已发布 published: 已发布
draft: 未发布 draft: 未发布
visible: visible:
label: 可见性 label: 可见性
result: "可见性:{visible}" result: "可见性:{visible}"
items: items:
all: 全部
public: 公开 public: 公开
private: 私有 private: 私有
author: author:

View File

@ -154,14 +154,12 @@ core:
filters: filters:
status: status:
items: items:
all: 全部
published: 已發布 published: 已發布
draft: 未發布 draft: 未發布
visible: visible:
label: 可見性 label: 可見性
result: "可見性:{visible}" result: "可見性:{visible}"
items: items:
all: 全部
public: 公開 public: 公開
private: 私有 private: 私有
category: category:
@ -324,14 +322,12 @@ core:
filters: filters:
status: status:
items: items:
all: 全部
published: 已發布 published: 已發布
draft: 未發布 draft: 未發布
visible: visible:
label: 可見性 label: 可見性
result: "可見性:{visible}" result: "可見性:{visible}"
items: items:
all: 全部
public: 公開 public: 公開
private: 私有 private: 私有
author: author:

View File

@ -25,13 +25,12 @@ import {
VDropdownItem, VDropdownItem,
} from "@halo-dev/components"; } from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue"; import LazyImage from "@/components/image/LazyImage.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import AttachmentDetailModal from "./components/AttachmentDetailModal.vue"; import AttachmentDetailModal from "./components/AttachmentDetailModal.vue";
import AttachmentUploadModal from "./components/AttachmentUploadModal.vue"; import AttachmentUploadModal from "./components/AttachmentUploadModal.vue";
import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue"; import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue";
import AttachmentGroupList from "./components/AttachmentGroupList.vue"; import AttachmentGroupList from "./components/AttachmentGroupList.vue";
import { computed, onMounted, ref, watch } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import type { Attachment, Group, Policy, User } from "@halo-dev/api-client"; import type { Attachment, Group, Policy } from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { useFetchAttachmentPolicy } from "./composables/use-attachment-policy"; import { useFetchAttachmentPolicy } from "./composables/use-attachment-policy";
@ -46,6 +45,7 @@ import { usePermission } from "@/utils/permission";
import FilterTag from "@/components/filter/FilterTag.vue"; import FilterTag from "@/components/filter/FilterTag.vue";
import { getNode } from "@formkit/core"; import { getNode } from "@formkit/core";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { t } = useI18n(); const { t } = useI18n();
@ -85,7 +85,7 @@ const SortItems: SortItem[] = [
]; ];
const selectedPolicy = ref<Policy>(); const selectedPolicy = ref<Policy>();
const selectedUser = ref<User>(); const selectedUser = ref();
const selectedSortItem = ref<SortItem>(); const selectedSortItem = ref<SortItem>();
const selectedSortItemValue = computed(() => { const selectedSortItemValue = computed(() => {
return selectedSortItem.value?.value; return selectedSortItem.value?.value;
@ -96,11 +96,6 @@ function handleSelectPolicy(policy: Policy | undefined) {
page.value = 1; page.value = 1;
} }
function handleSelectUser(user: User | undefined) {
selectedUser.value = user;
page.value = 1;
}
function handleSortItemChange(sortItem?: SortItem) { function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem; selectedSortItem.value = sortItem;
page.value = 1; page.value = 1;
@ -386,17 +381,6 @@ onMounted(() => {
}} }}
</FilterTag> </FilterTag>
<FilterTag
v-if="selectedUser"
@close="handleSelectUser(undefined)"
>
{{
$t("core.attachment.filters.owner.result", {
owner: selectedUser.spec.displayName,
})
}}
</FilterTag>
<FilterTag <FilterTag
v-if="selectedSortItem" v-if="selectedSortItem"
@click="handleSortItemChange()" @click="handleSortItemChange()"
@ -464,21 +448,11 @@ onMounted(() => {
</VDropdownItem> </VDropdownItem>
</template> </template>
</VDropdown> </VDropdown>
<UserDropdownSelector
v-model:selected="selectedUser" <UserFilterDropdown
@select="handleSelectUser" v-model="selectedUser"
> :label="$t('core.attachment.filters.owner.label')"
<div />
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">
{{ $t("core.attachment.filters.owner.label") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
</UserDropdownSelector>
<!-- TODO: add filter by ref support --> <!-- TODO: add filter by ref support -->
<VDropdown v-if="false"> <VDropdown v-if="false">
<div <div

View File

@ -1,4 +1,4 @@
import type { Attachment, Group, Policy, User } from "@halo-dev/api-client"; import type { Attachment, Group, Policy } from "@halo-dev/api-client";
import { computed, type Ref } from "vue"; import { computed, type Ref } from "vue";
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import type { AttachmentLike } from "@halo-dev/console-shared"; import type { AttachmentLike } from "@halo-dev/console-shared";
@ -36,7 +36,7 @@ interface useAttachmentSelectReturn {
export function useAttachmentControl(filterOptions: { export function useAttachmentControl(filterOptions: {
policy?: Ref<Policy | undefined>; policy?: Ref<Policy | undefined>;
group?: Ref<Group | undefined>; group?: Ref<Group | undefined>;
user?: Ref<User | undefined>; user?: Ref<string | undefined>;
keyword?: Ref<string | undefined>; keyword?: Ref<string | undefined>;
sort?: Ref<string | undefined>; sort?: Ref<string | undefined>;
page: Ref<number>; page: Ref<number>;
@ -62,7 +62,7 @@ export function useAttachmentControl(filterOptions: {
displayName: keyword?.value, displayName: keyword?.value,
group: group?.value?.metadata.name, group: group?.value?.metadata.name,
ungrouped: group?.value?.metadata.name === "ungrouped", ungrouped: group?.value?.metadata.name === "ungrouped",
uploadedBy: user?.value?.metadata.name, uploadedBy: user?.value,
page: page?.value, page: page?.value,
size: size?.value, size: size?.value,
sort: [sort?.value as string].filter(Boolean), sort: [sort?.value as string].filter(Boolean),

View File

@ -16,14 +16,14 @@ import {
Toast, Toast,
} from "@halo-dev/components"; } from "@halo-dev/components";
import CommentListItem from "./components/CommentListItem.vue"; import CommentListItem from "./components/CommentListItem.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue"; import type { ListedComment } from "@halo-dev/api-client";
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 { 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";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
const { t } = useI18n(); const { t } = useI18n();
@ -87,7 +87,7 @@ const selectedApprovedFilterItem = ref<{ label: string; value?: boolean }>(
const selectedSortItem = ref<SortItem>(); const selectedSortItem = ref<SortItem>();
const selectedUser = ref<User>(); const selectedUser = ref();
const handleApprovedFilterItemChange = (filterItem: { const handleApprovedFilterItemChange = (filterItem: {
label: string; label: string;
@ -104,11 +104,6 @@ const handleSortItemChange = (sortItem: SortItem) => {
page.value = 1; page.value = 1;
}; };
function handleSelectUser(user: User | undefined) {
selectedUser.value = user;
page.value = 1;
}
function handleKeywordChange() { function handleKeywordChange() {
const keywordNode = getNode("keywordInput"); const keywordNode = getNode("keywordInput");
if (keywordNode) { if (keywordNode) {
@ -165,7 +160,7 @@ const {
approved: selectedApprovedFilterItem.value.value, approved: selectedApprovedFilterItem.value.value,
sort: [selectedSortItem.value?.sort].filter(Boolean) as string[], sort: [selectedSortItem.value?.sort].filter(Boolean) as string[],
keyword: keyword.value, keyword: keyword.value,
ownerName: selectedUser.value?.metadata.name, ownerName: selectedUser.value,
}); });
total.value = data.total; total.value = data.total;
@ -345,17 +340,6 @@ const handleApproveInBatch = async () => {
}} }}
</FilterTag> </FilterTag>
<FilterTag
v-if="selectedUser"
@close="handleSelectUser(undefined)"
>
{{
$t("core.comment.filters.owner.result", {
owner: selectedUser.spec.displayName,
})
}}
</FilterTag>
<FilterTag <FilterTag
v-if="selectedSortItem" v-if="selectedSortItem"
@close="handleSortItemChange(SortItems[0])" @close="handleSortItemChange(SortItems[0])"
@ -411,21 +395,10 @@ const handleApproveInBatch = async () => {
</VDropdownItem> </VDropdownItem>
</template> </template>
</VDropdown> </VDropdown>
<UserDropdownSelector <UserFilterDropdown
v-model:selected="selectedUser" v-model="selectedUser"
@select="handleSelectUser" :label="$t('core.comment.filters.owner.label')"
> />
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">
{{ $t("core.comment.filters.owner.label") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
</UserDropdownSelector>
<VDropdown> <VDropdown>
<div <div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"

View File

@ -25,8 +25,6 @@ import { formatDatetime } from "@/utils/date";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import { getNode } from "@formkit/core";
import FilterTag from "@/components/filter/FilterTag.vue";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
@ -199,19 +197,12 @@ watch(selectedPageNames, (newValue) => {
checkedAll.value = newValue.length === singlePages.value?.length; checkedAll.value = newValue.length === singlePages.value?.length;
}); });
// Filters watch(
function handleKeywordChange() { () => keyword.value,
const keywordNode = getNode("keywordInput"); () => {
if (keywordNode) { page.value = 1;
keyword.value = keywordNode._value as string;
} }
page.value = 1; );
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
</script> </script>
<template> <template>
@ -256,28 +247,7 @@ function handleClearKeyword() {
/> />
</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="!selectedPageNames.length" v-model="keyword" />
v-if="!selectedPageNames.length"
class="flex items-center gap-2"
>
<FormKit
id="keywordInput"
outer-class="!p-0"
:placeholder="$t('core.common.placeholder.search')"
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>
</div>
<VSpace v-else> <VSpace v-else>
<VButton type="danger" @click="handleDeletePermanentlyInBatch"> <VButton type="danger" @click="handleDeletePermanentlyInBatch">
{{ $t("core.common.buttons.delete_permanently") }} {{ $t("core.common.buttons.delete_permanently") }}

View File

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconEye, IconEye,
@ -22,24 +21,21 @@ import {
VLoading, VLoading,
VPageHeader, VPageHeader,
Toast, Toast,
VDropdown,
VDropdownItem, VDropdownItem,
VDropdownDivider, VDropdownDivider,
} from "@halo-dev/components"; } from "@halo-dev/components";
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue"; import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import type { ListedSinglePage, SinglePage, User } from "@halo-dev/api-client"; import type { ListedSinglePage, SinglePage } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep"; 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 { 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";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { t } = useI18n(); const { t } = useI18n();
@ -50,126 +46,39 @@ const selectedPageNames = ref<string[]>([]);
const checkedAll = ref(false); const checkedAll = ref(false);
// Filters // Filters
interface VisibleItem { const selectedContributor = ref();
label: string; const selectedVisible = ref();
value?: "PUBLIC" | "INTERNAL" | "PRIVATE"; const selectedPublishStatus = ref();
} const selectedSortValue = ref();
interface PublishStatusItem {
label: string;
value?: boolean;
}
interface SortItem {
label: string;
sort: string;
}
const VisibleItems: VisibleItem[] = [
{
label: t("core.page.filters.visible.items.all"),
value: undefined,
},
{
label: t("core.page.filters.visible.items.public"),
value: "PUBLIC",
},
{
label: t("core.page.filters.visible.items.private"),
value: "PRIVATE",
},
];
const PublishStatusItems: PublishStatusItem[] = [
{
label: t("core.page.filters.status.items.all"),
value: undefined,
},
{
label: t("core.page.filters.status.items.published"),
value: true,
},
{
label: t("core.page.filters.status.items.draft"),
value: false,
},
];
const SortItems: SortItem[] = [
{
label: t("core.page.filters.sort.items.publish_time_desc"),
sort: "publishTime,desc",
},
{
label: t("core.page.filters.sort.items.publish_time_asc"),
sort: "publishTime,asc",
},
{
label: t("core.page.filters.sort.items.create_time_desc"),
sort: "creationTimestamp,desc",
},
{
label: t("core.page.filters.sort.items.create_time_asc"),
sort: "creationTimestamp,asc",
},
];
const selectedContributor = ref<User>();
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
const selectedPublishStatusItem = ref<PublishStatusItem>(PublishStatusItems[0]);
const selectedSortItem = ref<SortItem>();
const keyword = ref(""); const keyword = ref("");
function handleVisibleItemChange(visibleItem: VisibleItem) { watch(
selectedVisibleItem.value = visibleItem; () => [
page.value = 1; selectedContributor.value,
} selectedVisible.value,
selectedPublishStatus.value,
const handleSelectUser = (user?: User) => { selectedSortValue.value,
selectedContributor.value = user; keyword.value,
page.value = 1; ],
}; () => {
page.value = 1;
function handlePublishStatusItemChange(publishStatusItem: PublishStatusItem) {
selectedPublishStatusItem.value = publishStatusItem;
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 (
selectedContributor.value || selectedContributor.value ||
selectedVisibleItem.value.value || selectedVisible.value ||
selectedPublishStatusItem.value.value !== undefined || selectedPublishStatus.value !== undefined ||
selectedSortItem.value || selectedSortValue.value
keyword.value
); );
}); });
function handleClearFilters() { function handleClearFilters() {
selectedContributor.value = undefined; selectedContributor.value = undefined;
selectedVisibleItem.value = VisibleItems[0]; selectedVisible.value = undefined;
selectedPublishStatusItem.value = PublishStatusItems[0]; selectedPublishStatus.value = undefined;
selectedSortItem.value = undefined; selectedSortValue.value = undefined;
keyword.value = "";
page.value = 1;
} }
const page = ref(1); const page = ref(1);
@ -187,11 +96,11 @@ const {
queryKey: [ queryKey: [
"singlePages", "singlePages",
selectedContributor, selectedContributor,
selectedPublishStatusItem, selectedPublishStatus,
page, page,
size, size,
selectedVisibleItem, selectedVisible,
selectedSortItem, selectedSortValue,
keyword, keyword,
], ],
queryFn: async () => { queryFn: async () => {
@ -199,12 +108,12 @@ const {
const labelSelector: string[] = ["content.halo.run/deleted=false"]; const labelSelector: string[] = ["content.halo.run/deleted=false"];
if (selectedContributor.value) { if (selectedContributor.value) {
contributors = [selectedContributor.value.metadata.name]; contributors = [selectedContributor.value];
} }
if (selectedPublishStatusItem.value.value !== undefined) { if (selectedPublishStatus.value !== undefined) {
labelSelector.push( labelSelector.push(
`${singlePageLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}` `${singlePageLabels.PUBLISHED}=${selectedPublishStatus.value}`
); );
} }
@ -212,8 +121,8 @@ const {
labelSelector, labelSelector,
page: page.value, page: page.value,
size: size.value, size: size.value,
visible: selectedVisibleItem.value.value, visible: selectedVisible.value,
sort: [selectedSortItem.value?.sort].filter(Boolean) as string[], sort: [selectedSortValue.value].filter(Boolean) as string[],
keyword: keyword.value, keyword: keyword.value,
contributor: contributors, contributor: contributors,
}); });
@ -497,77 +406,7 @@ const getExternalUrl = (singlePage: SinglePage) => {
/> />
</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="!selectedPageNames.length" v-model="keyword" />
v-if="!selectedPageNames.length"
class="flex items-center gap-2"
>
<FormKit
id="keywordInput"
outer-class="!p-0"
:placeholder="$t('core.common.placeholder.search')"
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="selectedPublishStatusItem.value !== undefined"
@close="handlePublishStatusItemChange(PublishStatusItems[0])"
>
{{
$t("core.common.filters.results.status", {
status: selectedPublishStatusItem.label,
})
}}
</FilterTag>
<FilterTag
v-if="selectedVisibleItem.value"
@close="handleVisibleItemChange(VisibleItems[0])"
>
{{
$t("core.page.filters.visible.result", {
visible: selectedVisibleItem.label,
})
}}
</FilterTag>
<FilterTag
v-if="selectedContributor"
@close="handleSelectUser()"
>
{{
$t("core.page.filters.author.result", {
author: selectedContributor.spec.displayName,
})
}}
</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") }}
@ -576,89 +415,77 @@ const getExternalUrl = (singlePage: SinglePage) => {
</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.common.filters.labels.status") }} v-model="selectedPublishStatus"
</span> :label="$t('core.common.filters.labels.status')"
<span> :items="[
<IconArrowDown /> {
</span> label: t('core.common.filters.item_labels.all'),
</div> value: undefined,
<template #popper> },
<VDropdownItem {
v-for="(filterItem, index) in PublishStatusItems" label: t('core.page.filters.status.items.published'),
:key="index" value: true,
:selected=" },
filterItem.value === selectedPublishStatusItem.value {
" label: t('core.page.filters.status.items.draft'),
@click="handlePublishStatusItemChange(filterItem)" value: false,
> },
{{ filterItem.label }} ]"
</VDropdownItem> />
</template> <FilterDropdown
</VDropdown> v-model="selectedVisible"
<VDropdown> :label="$t('core.page.filters.visible.label')"
<div :items="[
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"> value: undefined,
{{ $t("core.page.filters.visible.label") }} },
</span> {
<span> label: t('core.page.filters.visible.items.public'),
<IconArrowDown /> value: 'PUBLIC',
</span> },
</div> {
<template #popper> label: t('core.page.filters.visible.items.private'),
<VDropdownItem value: 'PRIVATE',
v-for="(filterItem, index) in VisibleItems" },
:key="index" ]"
:selected="filterItem.value === selectedVisibleItem.value" />
@click="handleVisibleItemChange(filterItem)" <UserFilterDropdown
> v-model="selectedContributor"
{{ filterItem.label }} :label="$t('core.page.filters.author.label')"
</VDropdownItem> />
</template> <FilterDropdown
</VDropdown> v-model="selectedSortValue"
<UserDropdownSelector :label="$t('core.common.filters.labels.sort')"
v-model:selected="selectedContributor" :items="[
@select="handleSelectUser" {
> label: t('core.common.filters.item_labels.default'),
<div },
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" {
> label: t(
<span class="mr-0.5"> 'core.page.filters.sort.items.publish_time_desc'
{{ $t("core.page.filters.author.label") }} ),
</span> value: 'publishTime,desc',
<span> },
<IconArrowDown /> {
</span> label: t('core.page.filters.sort.items.publish_time_asc'),
</div> value: 'publishTime,asc',
</UserDropdownSelector> },
<VDropdown> {
<div label: t('core.page.filters.sort.items.create_time_desc'),
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" value: 'creationTimestamp,desc',
> },
<span class="mr-0.5"> {
{{ $t("core.common.filters.labels.sort") }} label: t('core.page.filters.sort.items.create_time_asc'),
</span> value: 'creationTimestamp,asc',
<span> },
<IconArrowDown /> ]"
</span> />
</div>
<template #popper>
<VDropdownItem
v-for="(sortItem, index) in SortItems"
:key="index"
:selected="sortItem.sort === selectedSortItem?.sort"
@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"

View File

@ -25,8 +25,6 @@ import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { getNode } from "@formkit/core";
import FilterTag from "@/components/filter/FilterTag.vue";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
@ -191,18 +189,12 @@ watch(selectedPostNames, (newValue) => {
checkedAll.value = newValue.length === posts.value?.length; checkedAll.value = newValue.length === posts.value?.length;
}); });
function handleKeywordChange() { watch(
const keywordNode = getNode("keywordInput"); () => keyword.value,
if (keywordNode) { () => {
keyword.value = keywordNode._value as string; page.value = 1;
} }
page.value = 1; );
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
</script> </script>
<template> <template>
<VPageHeader :title="$t('core.deleted_post.title')"> <VPageHeader :title="$t('core.deleted_post.title')">
@ -247,28 +239,7 @@ function handleClearKeyword() {
/> />
</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="!selectedPostNames.length" v-model="keyword" />
v-if="!selectedPostNames.length"
class="flex items-center gap-2"
>
<FormKit
id="keywordInput"
outer-class="!p-0"
:placeholder="$t('core.common.placeholder.search')"
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>
</div>
<VSpace v-else> <VSpace v-else>
<VButton type="danger" @click="handleDeletePermanentlyInBatch"> <VButton type="danger" @click="handleDeletePermanentlyInBatch">
{{ $t("core.common.buttons.delete_permanently") }} {{ $t("core.common.buttons.delete_permanently") }}

View File

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
IconAddCircle, IconAddCircle,
IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconBookRead, IconBookRead,
@ -23,30 +22,21 @@ import {
VLoading, VLoading,
Toast, Toast,
VDropdownItem, VDropdownItem,
VDropdown,
VDropdownDivider, VDropdownDivider,
} from "@halo-dev/components"; } from "@halo-dev/components";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import CategoryDropdownSelector from "@/components/dropdown-selector/CategoryDropdownSelector.vue";
import PostSettingModal from "./components/PostSettingModal.vue"; import PostSettingModal from "./components/PostSettingModal.vue";
import PostTag from "../posts/tags/components/PostTag.vue"; import PostTag from "../posts/tags/components/PostTag.vue";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import type { import type { Post, ListedPost } from "@halo-dev/api-client";
User,
Category,
Post,
Tag,
ListedPost,
} from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date"; 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 { getNode } from "@formkit/core";
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
import { useMutation, useQuery } from "@tanstack/vue-query"; import { useMutation, useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
import CategoryFilterDropdown from "@/components/filter/CategoryFilterDropdown.vue";
import TagFilterDropdown from "@/components/filter/TagFilterDropdown.vue";
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { t } = useI18n(); const { t } = useI18n();
@ -57,141 +47,46 @@ const checkedAll = ref(false);
const selectedPostNames = ref<string[]>([]); const selectedPostNames = ref<string[]>([]);
// Filters // Filters
interface VisibleItem { const selectedVisible = ref();
label: string; const selectedPublishStatus = ref();
value?: "PUBLIC" | "INTERNAL" | "PRIVATE"; const selectedSort = ref();
} const selectedCategory = ref();
const selectedTag = ref();
interface PublishStatusItem { const selectedContributor = ref();
label: string;
value?: boolean;
}
interface SortItem {
label: string;
sort: string;
}
const VisibleItems: VisibleItem[] = [
{
label: t("core.post.filters.visible.items.all"),
value: undefined,
},
{
label: t("core.post.filters.visible.items.public"),
value: "PUBLIC",
},
{
label: t("core.post.filters.visible.items.private"),
value: "PRIVATE",
},
];
const PublishStatusItems: PublishStatusItem[] = [
{
label: t("core.post.filters.status.items.all"),
value: undefined,
},
{
label: t("core.post.filters.status.items.published"),
value: true,
},
{
label: t("core.post.filters.status.items.draft"),
value: false,
},
];
const SortItems: SortItem[] = [
{
label: t("core.post.filters.sort.items.publish_time_desc"),
sort: "publishTime,desc",
},
{
label: t("core.post.filters.sort.items.publish_time_asc"),
sort: "publishTime,asc",
},
{
label: t("core.post.filters.sort.items.create_time_desc"),
sort: "creationTimestamp,desc",
},
{
label: t("core.post.filters.sort.items.create_time_asc"),
sort: "creationTimestamp,asc",
},
];
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
const selectedPublishStatusItem = ref<PublishStatusItem>(PublishStatusItems[0]);
const selectedSortItem = ref<SortItem>();
const selectedCategory = ref<Category>();
const selectedTag = ref<Tag>();
const selectedContributor = ref<User>();
const keyword = ref(""); const keyword = ref("");
function handleVisibleItemChange(visibleItem: VisibleItem) { watch(
selectedVisibleItem.value = visibleItem; () => [
page.value = 1; selectedVisible.value,
} selectedPublishStatus.value,
selectedSort.value,
function handlePublishStatusItemChange(publishStatusItem: PublishStatusItem) { selectedCategory.value,
selectedPublishStatusItem.value = publishStatusItem; selectedTag.value,
page.value = 1; selectedContributor.value,
} keyword.value,
],
function handleSortItemChange(sortItem?: SortItem) { () => {
selectedSortItem.value = sortItem; page.value = 1;
page.value = 1;
}
function handleCategoryChange(category?: Category) {
selectedCategory.value = category;
page.value = 1;
}
function handleTagChange(tag?: Tag) {
selectedTag.value = tag;
page.value = 1;
}
function handleContributorChange(user?: User) {
selectedContributor.value = user;
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;
}
function handleClearFilters() { function handleClearFilters() {
selectedVisibleItem.value = VisibleItems[0]; selectedVisible.value = undefined;
selectedPublishStatusItem.value = PublishStatusItems[0]; selectedPublishStatus.value = undefined;
selectedSortItem.value = undefined; selectedSort.value = undefined;
selectedCategory.value = undefined; selectedCategory.value = undefined;
selectedTag.value = undefined; selectedTag.value = undefined;
selectedContributor.value = undefined; selectedContributor.value = undefined;
keyword.value = "";
page.value = 1;
} }
const hasFilters = computed(() => { const hasFilters = computed(() => {
return ( return (
selectedVisibleItem.value.value || selectedVisible.value ||
selectedPublishStatusItem.value.value !== undefined || selectedPublishStatus.value !== undefined ||
selectedSortItem.value || selectedSort.value ||
selectedCategory.value || selectedCategory.value ||
selectedTag.value || selectedTag.value ||
selectedContributor.value || selectedContributor.value
keyword.value
); );
}); });
@ -214,9 +109,9 @@ const {
selectedCategory, selectedCategory,
selectedTag, selectedTag,
selectedContributor, selectedContributor,
selectedPublishStatusItem, selectedPublishStatus,
selectedVisibleItem, selectedVisible,
selectedSortItem, selectedSort,
keyword, keyword,
], ],
queryFn: async () => { queryFn: async () => {
@ -226,20 +121,20 @@ const {
const labelSelector: string[] = ["content.halo.run/deleted=false"]; const labelSelector: string[] = ["content.halo.run/deleted=false"];
if (selectedCategory.value) { if (selectedCategory.value) {
categories = [selectedCategory.value.metadata.name]; categories = [selectedCategory.value];
} }
if (selectedTag.value) { if (selectedTag.value) {
tags = [selectedTag.value.metadata.name]; tags = [selectedTag.value];
} }
if (selectedContributor.value) { if (selectedContributor.value) {
contributors = [selectedContributor.value.metadata.name]; contributors = [selectedContributor.value];
} }
if (selectedPublishStatusItem.value.value !== undefined) { if (selectedPublishStatus.value !== undefined) {
labelSelector.push( labelSelector.push(
`${postLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}` `${postLabels.PUBLISHED}=${selectedPublishStatus.value}`
); );
} }
@ -247,8 +142,8 @@ const {
labelSelector, labelSelector,
page: page.value, page: page.value,
size: size.value, size: size.value,
visible: selectedVisibleItem.value?.value, visible: selectedVisible.value,
sort: [selectedSortItem.value?.sort].filter(Boolean) as string[], sort: [selectedSort.value?.sort].filter(Boolean) as string[],
keyword: keyword.value, keyword: keyword.value,
category: categories, category: categories,
tag: tags, tag: tags,
@ -511,96 +406,7 @@ const getExternalUrl = (post: Post) => {
/> />
</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="!selectedPostNames.length" v-model="keyword" />
v-if="!selectedPostNames.length"
class="flex items-center gap-2"
>
<FormKit
id="keywordInput"
outer-class="!p-0"
:placeholder="$t('core.common.placeholder.search')"
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="selectedPublishStatusItem.value !== undefined"
@close="handlePublishStatusItemChange(PublishStatusItems[0])"
>
{{
$t("core.common.filters.results.status", {
status: selectedPublishStatusItem.label,
})
}}
</FilterTag>
<FilterTag
v-if="selectedVisibleItem.value"
@close="handleVisibleItemChange(VisibleItems[0])"
>
{{
$t("core.post.filters.visible.result", {
visible: selectedVisibleItem.label,
})
}}
</FilterTag>
<FilterTag
v-if="selectedCategory"
@close="handleCategoryChange()"
>
{{
$t("core.post.filters.category.result", {
category: selectedCategory.spec.displayName,
})
}}
</FilterTag>
<FilterTag v-if="selectedTag" @click="handleTagChange()">
{{
$t("core.post.filters.tag.result", {
tag: selectedTag.spec.displayName,
})
}}
</FilterTag>
<FilterTag
v-if="selectedContributor"
@close="handleContributorChange()"
>
{{
$t("core.post.filters.author.result", {
author: selectedContributor.spec.displayName,
})
}}
</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") }}
@ -609,119 +415,85 @@ const getExternalUrl = (post: Post) => {
</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.common.filters.labels.status") }} v-model="selectedPublishStatus"
</span> :label="$t('core.common.filters.labels.status')"
<span> :items="[
<IconArrowDown /> {
</span> label: t('core.common.filters.item_labels.all'),
</div> value: undefined,
<template #popper> },
<VDropdownItem {
v-for="(filterItem, index) in PublishStatusItems" label: t('core.post.filters.status.items.published'),
:key="index" value: true,
:selected=" },
filterItem.value === selectedPublishStatusItem.value {
" label: t('core.post.filters.status.items.draft'),
@click="handlePublishStatusItemChange(filterItem)" value: false,
> },
{{ filterItem.label }} ]"
</VDropdownItem> />
</template> <FilterDropdown
</VDropdown> v-model="selectedVisible"
<VDropdown> :label="$t('core.post.filters.visible.label')"
<div :items="[
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"> value: undefined,
{{ $t("core.post.filters.visible.label") }} },
</span> {
<span> label: t('core.post.filters.visible.items.public'),
<IconArrowDown /> value: 'PUBLIC',
</span> },
</div> {
<template #popper> label: t('core.post.filters.visible.items.private'),
<VDropdownItem value: 'PRIVATE',
v-for="(filterItem, index) in VisibleItems" },
:key="index" ]"
:selected="filterItem.value === selectedVisibleItem.value" />
@click="handleVisibleItemChange(filterItem)" <CategoryFilterDropdown
> v-model="selectedCategory"
{{ filterItem.label }} :label="$t('core.post.filters.category.label')"
</VDropdownItem> />
</template> <TagFilterDropdown
</VDropdown> v-model="selectedTag"
<CategoryDropdownSelector :label="$t('core.post.filters.tag.label')"
v-model:selected="selectedCategory" />
@select="handleCategoryChange" <UserFilterDropdown
> v-model="selectedContributor"
<div :label="$t('core.post.filters.author.label')"
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" />
> <FilterDropdown
<span class="mr-0.5"> v-model="selectedSort"
{{ $t("core.post.filters.category.label") }} :label="$t('core.common.filters.labels.sort')"
</span> :items="[
<span> {
<IconArrowDown /> label: t('core.common.filters.item_labels.default'),
</span> },
</div> {
</CategoryDropdownSelector> label: t(
<TagDropdownSelector 'core.post.filters.sort.items.publish_time_desc'
v-model:selected="selectedTag" ),
@select="handleTagChange" value: 'publishTime,desc',
> },
<div {
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" label: t('core.post.filters.sort.items.publish_time_asc'),
> value: 'publishTime,asc',
<span class="mr-0.5"> },
{{ $t("core.post.filters.tag.label") }} {
</span> label: t('core.post.filters.sort.items.create_time_desc'),
<span> value: 'creationTimestamp,desc',
<IconArrowDown /> },
</span> {
</div> label: t('core.post.filters.sort.items.create_time_asc'),
</TagDropdownSelector> value: 'creationTimestamp,asc',
<UserDropdownSelector },
v-model:selected="selectedContributor" ]"
@select="handleContributorChange" />
>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">
{{ $t("core.post.filters.author.label") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
</UserDropdownSelector>
<VDropdown>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">
{{ $t("core.common.filters.labels.sort") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<VDropdownItem
v-for="(sortItem, index) in SortItems"
:key="index"
:selected="sortItem.sort === selectedSortItem?.sort"
@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"