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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,13 +25,12 @@ import {
VDropdownItem,
} from "@halo-dev/components";
import LazyImage from "@/components/image/LazyImage.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import AttachmentDetailModal from "./components/AttachmentDetailModal.vue";
import AttachmentUploadModal from "./components/AttachmentUploadModal.vue";
import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue";
import AttachmentGroupList from "./components/AttachmentGroupList.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 prettyBytes from "pretty-bytes";
import { useFetchAttachmentPolicy } from "./composables/use-attachment-policy";
@ -46,6 +45,7 @@ import { usePermission } from "@/utils/permission";
import FilterTag from "@/components/filter/FilterTag.vue";
import { getNode } from "@formkit/core";
import { useI18n } from "vue-i18n";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -85,7 +85,7 @@ const SortItems: SortItem[] = [
];
const selectedPolicy = ref<Policy>();
const selectedUser = ref<User>();
const selectedUser = ref();
const selectedSortItem = ref<SortItem>();
const selectedSortItemValue = computed(() => {
return selectedSortItem.value?.value;
@ -96,11 +96,6 @@ function handleSelectPolicy(policy: Policy | undefined) {
page.value = 1;
}
function handleSelectUser(user: User | undefined) {
selectedUser.value = user;
page.value = 1;
}
function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem;
page.value = 1;
@ -386,17 +381,6 @@ onMounted(() => {
}}
</FilterTag>
<FilterTag
v-if="selectedUser"
@close="handleSelectUser(undefined)"
>
{{
$t("core.attachment.filters.owner.result", {
owner: selectedUser.spec.displayName,
})
}}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
@click="handleSortItemChange()"
@ -464,21 +448,11 @@ onMounted(() => {
</VDropdownItem>
</template>
</VDropdown>
<UserDropdownSelector
v-model:selected="selectedUser"
@select="handleSelectUser"
>
<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>
<UserFilterDropdown
v-model="selectedUser"
:label="$t('core.attachment.filters.owner.label')"
/>
<!-- TODO: add filter by ref support -->
<VDropdown v-if="false">
<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 { ref, watch } from "vue";
import type { AttachmentLike } from "@halo-dev/console-shared";
@ -36,7 +36,7 @@ interface useAttachmentSelectReturn {
export function useAttachmentControl(filterOptions: {
policy?: Ref<Policy | undefined>;
group?: Ref<Group | undefined>;
user?: Ref<User | undefined>;
user?: Ref<string | undefined>;
keyword?: Ref<string | undefined>;
sort?: Ref<string | undefined>;
page: Ref<number>;
@ -62,7 +62,7 @@ export function useAttachmentControl(filterOptions: {
displayName: keyword?.value,
group: group?.value?.metadata.name,
ungrouped: group?.value?.metadata.name === "ungrouped",
uploadedBy: user?.value?.metadata.name,
uploadedBy: user?.value,
page: page?.value,
size: size?.value,
sort: [sort?.value as string].filter(Boolean),

View File

@ -16,14 +16,14 @@ import {
Toast,
} from "@halo-dev/components";
import CommentListItem from "./components/CommentListItem.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import type { ListedComment, User } from "@halo-dev/api-client";
import type { ListedComment } from "@halo-dev/api-client";
import { computed, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
import FilterTag from "@/components/filter/FilterTag.vue";
import { getNode } from "@formkit/core";
import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
const { t } = useI18n();
@ -87,7 +87,7 @@ const selectedApprovedFilterItem = ref<{ label: string; value?: boolean }>(
const selectedSortItem = ref<SortItem>();
const selectedUser = ref<User>();
const selectedUser = ref();
const handleApprovedFilterItemChange = (filterItem: {
label: string;
@ -104,11 +104,6 @@ const handleSortItemChange = (sortItem: SortItem) => {
page.value = 1;
};
function handleSelectUser(user: User | undefined) {
selectedUser.value = user;
page.value = 1;
}
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
@ -165,7 +160,7 @@ const {
approved: selectedApprovedFilterItem.value.value,
sort: [selectedSortItem.value?.sort].filter(Boolean) as string[],
keyword: keyword.value,
ownerName: selectedUser.value?.metadata.name,
ownerName: selectedUser.value,
});
total.value = data.total;
@ -345,17 +340,6 @@ const handleApproveInBatch = async () => {
}}
</FilterTag>
<FilterTag
v-if="selectedUser"
@close="handleSelectUser(undefined)"
>
{{
$t("core.comment.filters.owner.result", {
owner: selectedUser.spec.displayName,
})
}}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
@close="handleSortItemChange(SortItems[0])"
@ -411,21 +395,10 @@ const handleApproveInBatch = async () => {
</VDropdownItem>
</template>
</VDropdown>
<UserDropdownSelector
v-model:selected="selectedUser"
@select="handleSelectUser"
>
<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>
<UserFilterDropdown
v-model="selectedUser"
:label="$t('core.comment.filters.owner.label')"
/>
<VDropdown>
<div
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 cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
import { getNode } from "@formkit/core";
import FilterTag from "@/components/filter/FilterTag.vue";
import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
@ -199,19 +197,12 @@ watch(selectedPageNames, (newValue) => {
checkedAll.value = newValue.length === singlePages.value?.length;
});
// Filters
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
watch(
() => keyword.value,
() => {
page.value = 1;
}
page.value = 1;
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
);
</script>
<template>
@ -256,28 +247,7 @@ function handleClearKeyword() {
/>
</div>
<div class="flex w-full flex-1 items-center sm:w-auto">
<div
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>
<SearchInput v-if="!selectedPageNames.length" v-model="keyword" />
<VSpace v-else>
<VButton type="danger" @click="handleDeletePermanentlyInBatch">
{{ $t("core.common.buttons.delete_permanently") }}

View File

@ -1,6 +1,5 @@
<script lang="ts" setup>
import {
IconArrowDown,
IconArrowLeft,
IconArrowRight,
IconEye,
@ -22,24 +21,21 @@ import {
VLoading,
VPageHeader,
Toast,
VDropdown,
VDropdownItem,
VDropdownDivider,
} from "@halo-dev/components";
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.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 { formatDatetime } from "@/utils/date";
import { RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
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 { useI18n } from "vue-i18n";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -50,126 +46,39 @@ const selectedPageNames = ref<string[]>([]);
const checkedAll = ref(false);
// Filters
interface VisibleItem {
label: string;
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
}
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 selectedContributor = ref();
const selectedVisible = ref();
const selectedPublishStatus = ref();
const selectedSortValue = ref();
const keyword = ref("");
function handleVisibleItemChange(visibleItem: VisibleItem) {
selectedVisibleItem.value = visibleItem;
page.value = 1;
}
const handleSelectUser = (user?: User) => {
selectedContributor.value = user;
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;
watch(
() => [
selectedContributor.value,
selectedVisible.value,
selectedPublishStatus.value,
selectedSortValue.value,
keyword.value,
],
() => {
page.value = 1;
}
page.value = 1;
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
);
const hasFilters = computed(() => {
return (
selectedContributor.value ||
selectedVisibleItem.value.value ||
selectedPublishStatusItem.value.value !== undefined ||
selectedSortItem.value ||
keyword.value
selectedVisible.value ||
selectedPublishStatus.value !== undefined ||
selectedSortValue.value
);
});
function handleClearFilters() {
selectedContributor.value = undefined;
selectedVisibleItem.value = VisibleItems[0];
selectedPublishStatusItem.value = PublishStatusItems[0];
selectedSortItem.value = undefined;
keyword.value = "";
page.value = 1;
selectedVisible.value = undefined;
selectedPublishStatus.value = undefined;
selectedSortValue.value = undefined;
}
const page = ref(1);
@ -187,11 +96,11 @@ const {
queryKey: [
"singlePages",
selectedContributor,
selectedPublishStatusItem,
selectedPublishStatus,
page,
size,
selectedVisibleItem,
selectedSortItem,
selectedVisible,
selectedSortValue,
keyword,
],
queryFn: async () => {
@ -199,12 +108,12 @@ const {
const labelSelector: string[] = ["content.halo.run/deleted=false"];
if (selectedContributor.value) {
contributors = [selectedContributor.value.metadata.name];
contributors = [selectedContributor.value];
}
if (selectedPublishStatusItem.value.value !== undefined) {
if (selectedPublishStatus.value !== undefined) {
labelSelector.push(
`${singlePageLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}`
`${singlePageLabels.PUBLISHED}=${selectedPublishStatus.value}`
);
}
@ -212,8 +121,8 @@ const {
labelSelector,
page: page.value,
size: size.value,
visible: selectedVisibleItem.value.value,
sort: [selectedSortItem.value?.sort].filter(Boolean) as string[],
visible: selectedVisible.value,
sort: [selectedSortValue.value].filter(Boolean) as string[],
keyword: keyword.value,
contributor: contributors,
});
@ -497,77 +406,7 @@ const getExternalUrl = (singlePage: SinglePage) => {
/>
</div>
<div class="flex w-full flex-1 items-center sm:w-auto">
<div
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>
<SearchInput v-if="!selectedPageNames.length" v-model="keyword" />
<VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch">
{{ $t("core.common.buttons.delete") }}
@ -576,89 +415,77 @@ const getExternalUrl = (singlePage: SinglePage) => {
</div>
<div class="mt-4 flex sm:mt-0">
<VSpace spacing="lg">
<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.status") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<VDropdownItem
v-for="(filterItem, index) in PublishStatusItems"
:key="index"
:selected="
filterItem.value === selectedPublishStatusItem.value
"
@click="handlePublishStatusItemChange(filterItem)"
>
{{ filterItem.label }}
</VDropdownItem>
</template>
</VDropdown>
<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.page.filters.visible.label") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<VDropdownItem
v-for="(filterItem, index) in VisibleItems"
:key="index"
:selected="filterItem.value === selectedVisibleItem.value"
@click="handleVisibleItemChange(filterItem)"
>
{{ filterItem.label }}
</VDropdownItem>
</template>
</VDropdown>
<UserDropdownSelector
v-model:selected="selectedContributor"
@select="handleSelectUser"
>
<div
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
>
<span class="mr-0.5">
{{ $t("core.page.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>
<FilterCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
<FilterDropdown
v-model="selectedPublishStatus"
:label="$t('core.common.filters.labels.status')"
:items="[
{
label: t('core.common.filters.item_labels.all'),
value: undefined,
},
{
label: t('core.page.filters.status.items.published'),
value: true,
},
{
label: t('core.page.filters.status.items.draft'),
value: false,
},
]"
/>
<FilterDropdown
v-model="selectedVisible"
:label="$t('core.page.filters.visible.label')"
:items="[
{
label: t('core.common.filters.item_labels.all'),
value: undefined,
},
{
label: t('core.page.filters.visible.items.public'),
value: 'PUBLIC',
},
{
label: t('core.page.filters.visible.items.private'),
value: 'PRIVATE',
},
]"
/>
<UserFilterDropdown
v-model="selectedContributor"
:label="$t('core.page.filters.author.label')"
/>
<FilterDropdown
v-model="selectedSortValue"
:label="$t('core.common.filters.labels.sort')"
:items="[
{
label: t('core.common.filters.item_labels.default'),
},
{
label: t(
'core.page.filters.sort.items.publish_time_desc'
),
value: 'publishTime,desc',
},
{
label: t('core.page.filters.sort.items.publish_time_asc'),
value: 'publishTime,asc',
},
{
label: t('core.page.filters.sort.items.create_time_desc'),
value: 'creationTimestamp,desc',
},
{
label: t('core.page.filters.sort.items.create_time_asc'),
value: 'creationTimestamp,asc',
},
]"
/>
<div class="flex flex-row gap-2">
<div
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 { usePermission } from "@/utils/permission";
import cloneDeep from "lodash.clonedeep";
import { getNode } from "@formkit/core";
import FilterTag from "@/components/filter/FilterTag.vue";
import { useQuery } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
@ -191,18 +189,12 @@ watch(selectedPostNames, (newValue) => {
checkedAll.value = newValue.length === posts.value?.length;
});
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
watch(
() => keyword.value,
() => {
page.value = 1;
}
page.value = 1;
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
);
</script>
<template>
<VPageHeader :title="$t('core.deleted_post.title')">
@ -247,28 +239,7 @@ function handleClearKeyword() {
/>
</div>
<div class="flex w-full flex-1 items-center sm:w-auto">
<div
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>
<SearchInput v-if="!selectedPostNames.length" v-model="keyword" />
<VSpace v-else>
<VButton type="danger" @click="handleDeletePermanentlyInBatch">
{{ $t("core.common.buttons.delete_permanently") }}

View File

@ -1,7 +1,6 @@
<script lang="ts" setup>
import {
IconAddCircle,
IconArrowDown,
IconArrowLeft,
IconArrowRight,
IconBookRead,
@ -23,30 +22,21 @@ import {
VLoading,
Toast,
VDropdownItem,
VDropdown,
VDropdownDivider,
} 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 PostTag from "../posts/tags/components/PostTag.vue";
import { computed, ref, watch } from "vue";
import type {
User,
Category,
Post,
Tag,
ListedPost,
} from "@halo-dev/api-client";
import type { Post, ListedPost } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date";
import { usePermission } from "@/utils/permission";
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 { 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 { t } = useI18n();
@ -57,141 +47,46 @@ const checkedAll = ref(false);
const selectedPostNames = ref<string[]>([]);
// Filters
interface VisibleItem {
label: string;
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
}
interface PublishStatusItem {
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 selectedVisible = ref();
const selectedPublishStatus = ref();
const selectedSort = ref();
const selectedCategory = ref();
const selectedTag = ref();
const selectedContributor = ref();
const keyword = ref("");
function handleVisibleItemChange(visibleItem: VisibleItem) {
selectedVisibleItem.value = visibleItem;
page.value = 1;
}
function handlePublishStatusItemChange(publishStatusItem: PublishStatusItem) {
selectedPublishStatusItem.value = publishStatusItem;
page.value = 1;
}
function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem;
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;
watch(
() => [
selectedVisible.value,
selectedPublishStatus.value,
selectedSort.value,
selectedCategory.value,
selectedTag.value,
selectedContributor.value,
keyword.value,
],
() => {
page.value = 1;
}
page.value = 1;
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
);
function handleClearFilters() {
selectedVisibleItem.value = VisibleItems[0];
selectedPublishStatusItem.value = PublishStatusItems[0];
selectedSortItem.value = undefined;
selectedVisible.value = undefined;
selectedPublishStatus.value = undefined;
selectedSort.value = undefined;
selectedCategory.value = undefined;
selectedTag.value = undefined;
selectedContributor.value = undefined;
keyword.value = "";
page.value = 1;
}
const hasFilters = computed(() => {
return (
selectedVisibleItem.value.value ||
selectedPublishStatusItem.value.value !== undefined ||
selectedSortItem.value ||
selectedVisible.value ||
selectedPublishStatus.value !== undefined ||
selectedSort.value ||
selectedCategory.value ||
selectedTag.value ||
selectedContributor.value ||
keyword.value
selectedContributor.value
);
});
@ -214,9 +109,9 @@ const {
selectedCategory,
selectedTag,
selectedContributor,
selectedPublishStatusItem,
selectedVisibleItem,
selectedSortItem,
selectedPublishStatus,
selectedVisible,
selectedSort,
keyword,
],
queryFn: async () => {
@ -226,20 +121,20 @@ const {
const labelSelector: string[] = ["content.halo.run/deleted=false"];
if (selectedCategory.value) {
categories = [selectedCategory.value.metadata.name];
categories = [selectedCategory.value];
}
if (selectedTag.value) {
tags = [selectedTag.value.metadata.name];
tags = [selectedTag.value];
}
if (selectedContributor.value) {
contributors = [selectedContributor.value.metadata.name];
contributors = [selectedContributor.value];
}
if (selectedPublishStatusItem.value.value !== undefined) {
if (selectedPublishStatus.value !== undefined) {
labelSelector.push(
`${postLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}`
`${postLabels.PUBLISHED}=${selectedPublishStatus.value}`
);
}
@ -247,8 +142,8 @@ const {
labelSelector,
page: page.value,
size: size.value,
visible: selectedVisibleItem.value?.value,
sort: [selectedSortItem.value?.sort].filter(Boolean) as string[],
visible: selectedVisible.value,
sort: [selectedSort.value?.sort].filter(Boolean) as string[],
keyword: keyword.value,
category: categories,
tag: tags,
@ -511,96 +406,7 @@ const getExternalUrl = (post: Post) => {
/>
</div>
<div class="flex w-full flex-1 items-center sm:w-auto">
<div
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>
<SearchInput v-if="!selectedPostNames.length" v-model="keyword" />
<VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch">
{{ $t("core.common.buttons.delete") }}
@ -609,119 +415,85 @@ const getExternalUrl = (post: Post) => {
</div>
<div class="mt-4 flex sm:mt-0">
<VSpace spacing="lg">
<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.status") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<VDropdownItem
v-for="(filterItem, index) in PublishStatusItems"
:key="index"
:selected="
filterItem.value === selectedPublishStatusItem.value
"
@click="handlePublishStatusItemChange(filterItem)"
>
{{ filterItem.label }}
</VDropdownItem>
</template>
</VDropdown>
<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.post.filters.visible.label") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
<template #popper>
<VDropdownItem
v-for="(filterItem, index) in VisibleItems"
:key="index"
:selected="filterItem.value === selectedVisibleItem.value"
@click="handleVisibleItemChange(filterItem)"
>
{{ filterItem.label }}
</VDropdownItem>
</template>
</VDropdown>
<CategoryDropdownSelector
v-model:selected="selectedCategory"
@select="handleCategoryChange"
>
<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.category.label") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
</CategoryDropdownSelector>
<TagDropdownSelector
v-model:selected="selectedTag"
@select="handleTagChange"
>
<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.tag.label") }}
</span>
<span>
<IconArrowDown />
</span>
</div>
</TagDropdownSelector>
<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>
<FilterCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
<FilterDropdown
v-model="selectedPublishStatus"
:label="$t('core.common.filters.labels.status')"
:items="[
{
label: t('core.common.filters.item_labels.all'),
value: undefined,
},
{
label: t('core.post.filters.status.items.published'),
value: true,
},
{
label: t('core.post.filters.status.items.draft'),
value: false,
},
]"
/>
<FilterDropdown
v-model="selectedVisible"
:label="$t('core.post.filters.visible.label')"
:items="[
{
label: t('core.common.filters.item_labels.all'),
value: undefined,
},
{
label: t('core.post.filters.visible.items.public'),
value: 'PUBLIC',
},
{
label: t('core.post.filters.visible.items.private'),
value: 'PRIVATE',
},
]"
/>
<CategoryFilterDropdown
v-model="selectedCategory"
:label="$t('core.post.filters.category.label')"
/>
<TagFilterDropdown
v-model="selectedTag"
:label="$t('core.post.filters.tag.label')"
/>
<UserFilterDropdown
v-model="selectedContributor"
:label="$t('core.post.filters.author.label')"
/>
<FilterDropdown
v-model="selectedSort"
:label="$t('core.common.filters.labels.sort')"
:items="[
{
label: t('core.common.filters.item_labels.default'),
},
{
label: t(
'core.post.filters.sort.items.publish_time_desc'
),
value: 'publishTime,desc',
},
{
label: t('core.post.filters.sort.items.publish_time_asc'),
value: 'publishTime,asc',
},
{
label: t('core.post.filters.sort.items.create_time_desc'),
value: 'creationTimestamp,desc',
},
{
label: t('core.post.filters.sort.items.create_time_asc'),
value: 'creationTimestamp,asc',
},
]"
/>
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"