mirror of https://github.com/halo-dev/halo
Add subject-based comment list modal (#7681)
parent
2bcfbbc371
commit
f5af5a1550
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
|
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
|
||||||
import type { ListedComment } from "@halo-dev/api-client";
|
import type { ListedComment } from "@halo-dev/api-client";
|
||||||
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
|
import { coreApiClient } from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
IconMessage,
|
IconMessage,
|
||||||
|
@ -16,12 +16,12 @@ import {
|
||||||
VPagination,
|
VPagination,
|
||||||
VSpace,
|
VSpace,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { chunk } from "lodash-es";
|
import { chunk } from "lodash-es";
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import CommentListItem from "./components/CommentListItem.vue";
|
import CommentListItem from "./components/CommentListItem.vue";
|
||||||
|
import useCommentsFetch from "./composables/use-comments-fetch";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -72,58 +72,21 @@ const page = useRouteQuery<number>("page", 1, {
|
||||||
const size = useRouteQuery<number>("size", 20, {
|
const size = useRouteQuery<number>("size", 20, {
|
||||||
transform: Number,
|
transform: Number,
|
||||||
});
|
});
|
||||||
const total = ref(0);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: comments,
|
data: comments,
|
||||||
isLoading,
|
isLoading,
|
||||||
isFetching,
|
isFetching,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery<ListedComment[]>({
|
} = useCommentsFetch(
|
||||||
queryKey: [
|
"core:comments",
|
||||||
"core:comments",
|
page,
|
||||||
page,
|
size,
|
||||||
size,
|
selectedApprovedStatus,
|
||||||
selectedApprovedStatus,
|
selectedSort,
|
||||||
selectedSort,
|
selectedUser,
|
||||||
selectedUser,
|
keyword
|
||||||
keyword,
|
);
|
||||||
],
|
|
||||||
queryFn: async () => {
|
|
||||||
const fieldSelectorMap: Record<string, string | boolean | undefined> = {
|
|
||||||
"spec.approved": selectedApprovedStatus.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
const fieldSelector = Object.entries(fieldSelectorMap)
|
|
||||||
.map(([key, value]) => {
|
|
||||||
if (value !== undefined) {
|
|
||||||
return `${key}=${value}`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean) as string[];
|
|
||||||
|
|
||||||
const { data } = await consoleApiClient.content.comment.listComments({
|
|
||||||
fieldSelector,
|
|
||||||
page: page.value,
|
|
||||||
size: size.value,
|
|
||||||
sort: [selectedSort.value].filter(Boolean) as string[],
|
|
||||||
keyword: keyword.value,
|
|
||||||
ownerName: selectedUser.value,
|
|
||||||
// TODO: email users are not supported at the moment.
|
|
||||||
ownerKind: selectedUser.value ? "User" : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
total.value = data.total;
|
|
||||||
|
|
||||||
return data.items;
|
|
||||||
},
|
|
||||||
refetchInterval(data) {
|
|
||||||
const hasDeletingData = data?.some(
|
|
||||||
(comment) => !!comment.comment.metadata.deletionTimestamp
|
|
||||||
);
|
|
||||||
return hasDeletingData ? 1000 : false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
const handleCheckAllChange = (e: Event) => {
|
const handleCheckAllChange = (e: Event) => {
|
||||||
|
@ -131,7 +94,7 @@ const handleCheckAllChange = (e: Event) => {
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
selectedCommentNames.value =
|
selectedCommentNames.value =
|
||||||
comments.value?.map((comment) => {
|
comments.value?.items.map((comment) => {
|
||||||
return comment.comment.metadata.name;
|
return comment.comment.metadata.name;
|
||||||
}) || [];
|
}) || [];
|
||||||
} else {
|
} else {
|
||||||
|
@ -146,7 +109,7 @@ const isSelection = (comment: ListedComment) => {
|
||||||
watch(
|
watch(
|
||||||
() => selectedCommentNames.value,
|
() => selectedCommentNames.value,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
checkAll.value = newValue.length === comments.value?.length;
|
checkAll.value = newValue.length === comments.value?.items.length;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -192,7 +155,7 @@ const handleApproveInBatch = async () => {
|
||||||
cancelText: t("core.common.buttons.cancel"),
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
const commentsToUpdate = comments.value?.filter((comment) => {
|
const commentsToUpdate = comments.value?.items.filter((comment) => {
|
||||||
return (
|
return (
|
||||||
selectedCommentNames.value.includes(
|
selectedCommentNames.value.includes(
|
||||||
comment.comment.metadata.name
|
comment.comment.metadata.name
|
||||||
|
@ -370,7 +333,7 @@ const handleApproveInBatch = async () => {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<VLoading v-if="isLoading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition v-else-if="!comments?.length" appear name="fade">
|
<Transition v-else-if="!comments?.items.length" appear name="fade">
|
||||||
<VEmpty
|
<VEmpty
|
||||||
:message="$t('core.comment.empty.message')"
|
:message="$t('core.comment.empty.message')"
|
||||||
:title="$t('core.comment.empty.title')"
|
:title="$t('core.comment.empty.title')"
|
||||||
|
@ -385,7 +348,7 @@ const handleApproveInBatch = async () => {
|
||||||
<Transition v-else appear name="fade">
|
<Transition v-else appear name="fade">
|
||||||
<VEntityContainer>
|
<VEntityContainer>
|
||||||
<CommentListItem
|
<CommentListItem
|
||||||
v-for="comment in comments"
|
v-for="comment in comments.items"
|
||||||
:key="comment.comment.metadata.name"
|
:key="comment.comment.metadata.name"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
:is-selected="isSelection(comment)"
|
:is-selected="isSelection(comment)"
|
||||||
|
@ -409,9 +372,11 @@ const handleApproveInBatch = async () => {
|
||||||
:page-label="$t('core.components.pagination.page_label')"
|
:page-label="$t('core.components.pagination.page_label')"
|
||||||
:size-label="$t('core.components.pagination.size_label')"
|
:size-label="$t('core.components.pagination.size_label')"
|
||||||
:total-label="
|
:total-label="
|
||||||
$t('core.components.pagination.total_label', { total: total })
|
$t('core.components.pagination.total_label', {
|
||||||
|
total: comments?.total || 0,
|
||||||
|
})
|
||||||
"
|
"
|
||||||
:total="total"
|
:total="comments?.total || 0"
|
||||||
:size-options="[20, 30, 50, 100]"
|
:size-options="[20, 30, 50, 100]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -275,7 +275,9 @@ const { data: contentProvider } = useContentProviderExtensionPoint();
|
||||||
/>
|
/>
|
||||||
<VEntity :is-selected="isSelected">
|
<VEntity :is-selected="isSelected">
|
||||||
<template
|
<template
|
||||||
v-if="currentUserHasPermission(['system:comments:manage'])"
|
v-if="
|
||||||
|
currentUserHasPermission(['system:comments:manage']) && $slots.checkbox
|
||||||
|
"
|
||||||
#checkbox
|
#checkbox
|
||||||
>
|
>
|
||||||
<slot name="checkbox" />
|
<slot name="checkbox" />
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||||
|
import FilterDropdown from "@/components/filter/FilterDropdown.vue";
|
||||||
|
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
|
||||||
|
import SearchInput from "@/components/input/SearchInput.vue";
|
||||||
|
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||||
|
import {
|
||||||
|
IconRefreshLine,
|
||||||
|
VButton,
|
||||||
|
VEmpty,
|
||||||
|
VEntityContainer,
|
||||||
|
VLoading,
|
||||||
|
VPagination,
|
||||||
|
VSpace,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import { computed, ref, toRefs, watch } from "vue";
|
||||||
|
import useCommentsFetch from "../composables/use-comments-fetch";
|
||||||
|
import CommentListItem from "./CommentListItem.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
subjectRefKey: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { subjectRefKey } = toRefs(props);
|
||||||
|
|
||||||
|
const selectedApprovedStatus = ref();
|
||||||
|
const selectedSort = ref();
|
||||||
|
const selectedUser = ref();
|
||||||
|
const page = ref(1);
|
||||||
|
const size = ref(20);
|
||||||
|
const keyword = ref("");
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: comments,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
refetch,
|
||||||
|
} = useCommentsFetch(
|
||||||
|
"core:comments:with-subject",
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
selectedApprovedStatus,
|
||||||
|
selectedSort,
|
||||||
|
selectedUser,
|
||||||
|
keyword,
|
||||||
|
subjectRefKey
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
selectedApprovedStatus.value,
|
||||||
|
selectedSort.value,
|
||||||
|
selectedUser.value,
|
||||||
|
keyword.value,
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
page.value = 1;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasFilters = computed(() => {
|
||||||
|
return (
|
||||||
|
selectedApprovedStatus.value !== undefined ||
|
||||||
|
selectedSort.value ||
|
||||||
|
selectedUser.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClearFilters() {
|
||||||
|
selectedApprovedStatus.value = undefined;
|
||||||
|
selectedSort.value = undefined;
|
||||||
|
selectedUser.value = undefined;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mb-4 flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<SearchInput v-model="keyword" />
|
||||||
|
<VSpace spacing="lg" class="flex-wrap">
|
||||||
|
<FilterCleanButton v-if="hasFilters" @click="handleClearFilters" />
|
||||||
|
<FilterDropdown
|
||||||
|
v-model="selectedApprovedStatus"
|
||||||
|
:label="$t('core.common.filters.labels.status')"
|
||||||
|
:items="[
|
||||||
|
{
|
||||||
|
label: $t('core.common.filters.item_labels.all'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('core.comment.filters.status.items.approved'),
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('core.comment.filters.status.items.pending_review'),
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<HasPermission :permissions="['system:users:view']">
|
||||||
|
<UserFilterDropdown
|
||||||
|
v-model="selectedUser"
|
||||||
|
:label="$t('core.comment.filters.owner.label')"
|
||||||
|
/>
|
||||||
|
</HasPermission>
|
||||||
|
<FilterDropdown
|
||||||
|
v-model="selectedSort"
|
||||||
|
:label="$t('core.common.filters.labels.sort')"
|
||||||
|
:items="[
|
||||||
|
{
|
||||||
|
label: $t('core.common.filters.item_labels.default'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('core.comment.filters.sort.items.last_reply_time_desc'),
|
||||||
|
value: 'status.lastReplyTime,desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('core.comment.filters.sort.items.last_reply_time_asc'),
|
||||||
|
value: 'status.lastReplyTime,asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('core.comment.filters.sort.items.reply_count_desc'),
|
||||||
|
value: 'status.replyCount,desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('core.comment.filters.sort.items.reply_count_asc'),
|
||||||
|
value: 'status.replyCount,asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('core.comment.filters.sort.items.create_time_desc'),
|
||||||
|
value: 'metadata.creationTimestamp,desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('core.comment.filters.sort.items.create_time_asc'),
|
||||||
|
value: 'metadata.creationTimestamp,asc',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<div
|
||||||
|
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
||||||
|
@click="refetch()"
|
||||||
|
>
|
||||||
|
<IconRefreshLine
|
||||||
|
v-tooltip="$t('core.common.buttons.refresh')"
|
||||||
|
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VSpace>
|
||||||
|
</div>
|
||||||
|
<VLoading v-if="isLoading" />
|
||||||
|
<Transition v-else-if="!comments?.items.length" appear name="fade">
|
||||||
|
<VEmpty
|
||||||
|
:message="$t('core.comment.empty.message')"
|
||||||
|
:title="$t('core.comment.empty.title')"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<VButton @click="refetch">
|
||||||
|
{{ $t("core.common.buttons.refresh") }}
|
||||||
|
</VButton>
|
||||||
|
</template>
|
||||||
|
</VEmpty>
|
||||||
|
</Transition>
|
||||||
|
<Transition v-else appear name="fade">
|
||||||
|
<div class="overflow-hidden rounded-base border">
|
||||||
|
<VEntityContainer>
|
||||||
|
<CommentListItem
|
||||||
|
v-for="comment in comments.items"
|
||||||
|
:key="comment.comment.metadata.name"
|
||||||
|
:comment="comment"
|
||||||
|
>
|
||||||
|
</CommentListItem>
|
||||||
|
</VEntityContainer>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
<div class="mt-4">
|
||||||
|
<VPagination
|
||||||
|
v-model:page="page"
|
||||||
|
v-model:size="size"
|
||||||
|
:page-label="$t('core.components.pagination.page_label')"
|
||||||
|
:size-label="$t('core.components.pagination.size_label')"
|
||||||
|
:total-label="
|
||||||
|
$t('core.components.pagination.total_label', {
|
||||||
|
total: comments?.total || 0,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:total="comments?.total || 0"
|
||||||
|
:size-options="[20, 30, 50, 100]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VButton, VModal } from "@halo-dev/components";
|
||||||
|
import { useTemplateRef } from "vue";
|
||||||
|
import SubjectQueryCommentList from "./SubjectQueryCommentList.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
subjectRefKey: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const modal = useTemplateRef<InstanceType<typeof VModal> | null>("modal");
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VModal
|
||||||
|
ref="modal"
|
||||||
|
:centered="false"
|
||||||
|
:width="1400"
|
||||||
|
:title="$t('core.comment.title')"
|
||||||
|
:layer-closable="true"
|
||||||
|
mount-to-body
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<SubjectQueryCommentList :subject-ref-key="props.subjectRefKey" />
|
||||||
|
<template #footer>
|
||||||
|
<VButton @click="modal?.close()">
|
||||||
|
{{ $t("core.common.buttons.close") }}
|
||||||
|
</VButton>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
</template>
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { consoleApiClient, type ListedCommentList } from "@halo-dev/api-client";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
|
||||||
|
export default function useCommentsFetch(
|
||||||
|
queryKey: string,
|
||||||
|
page: Ref<number>,
|
||||||
|
size: Ref<number>,
|
||||||
|
approved: Ref<boolean | undefined>,
|
||||||
|
sort: Ref<string | undefined>,
|
||||||
|
user: Ref<string | undefined>,
|
||||||
|
keyword: Ref<string | undefined>,
|
||||||
|
subjectRefKey?: Ref<string | undefined>
|
||||||
|
) {
|
||||||
|
return useQuery<ListedCommentList>({
|
||||||
|
queryKey: [queryKey, page, size, approved, sort, user, keyword],
|
||||||
|
queryFn: async () => {
|
||||||
|
const fieldSelectorMap: Record<string, string | boolean | undefined> = {
|
||||||
|
"spec.approved": approved.value,
|
||||||
|
"spec.subjectRef": subjectRefKey?.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldSelector = Object.entries(fieldSelectorMap)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
return `${key}=${value}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
const { data } = await consoleApiClient.content.comment.listComments({
|
||||||
|
fieldSelector,
|
||||||
|
page: page.value,
|
||||||
|
size: size.value,
|
||||||
|
sort: [sort.value].filter(Boolean) as string[],
|
||||||
|
keyword: keyword.value,
|
||||||
|
ownerName: user.value,
|
||||||
|
ownerKind: user.value ? "User" : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
refetchInterval(data) {
|
||||||
|
const hasDeletingData = data?.items.some(
|
||||||
|
(comment) => !!comment.comment.metadata.deletionTimestamp
|
||||||
|
);
|
||||||
|
return hasDeletingData ? 1000 : false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -3,8 +3,14 @@ import { IconMessage } from "@halo-dev/components";
|
||||||
import { definePlugin } from "@halo-dev/console-shared";
|
import { definePlugin } from "@halo-dev/console-shared";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
import CommentList from "./CommentList.vue";
|
import CommentList from "./CommentList.vue";
|
||||||
|
import SubjectQueryCommentList from "./components/SubjectQueryCommentList.vue";
|
||||||
|
import SubjectQueryCommentListModal from "./components/SubjectQueryCommentListModal.vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
|
components: {
|
||||||
|
SubjectQueryCommentList,
|
||||||
|
SubjectQueryCommentListModal,
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/comments",
|
path: "/comments",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { singlePageLabels } from "@/constants/labels";
|
import { singlePageLabels } from "@/constants/labels";
|
||||||
|
import SubjectQueryCommentListModal from "@console/modules/contents/comments/components/SubjectQueryCommentListModal.vue";
|
||||||
import type { ListedSinglePage } from "@halo-dev/api-client";
|
import type { ListedSinglePage } from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
IconExternalLinkLine,
|
IconExternalLinkLine,
|
||||||
|
@ -7,7 +8,8 @@ import {
|
||||||
VSpace,
|
VSpace,
|
||||||
VStatusDot,
|
VStatusDot,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -16,6 +18,8 @@ const props = withDefaults(
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const externalUrl = computed(() => {
|
const externalUrl = computed(() => {
|
||||||
const { metadata, status } = props.singlePage.page;
|
const { metadata, status } = props.singlePage.page;
|
||||||
if (metadata.labels?.[singlePageLabels.PUBLISHED] === "true") {
|
if (metadata.labels?.[singlePageLabels.PUBLISHED] === "true") {
|
||||||
|
@ -23,6 +27,30 @@ const externalUrl = computed(() => {
|
||||||
}
|
}
|
||||||
return `/preview/singlepages/${metadata.name}`;
|
return `/preview/singlepages/${metadata.name}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const commentSubjectRefKey = `content.halo.run/SinglePage/${props.singlePage.page.metadata.name}`;
|
||||||
|
const commentListVisible = ref(false);
|
||||||
|
|
||||||
|
const commentText = computed(() => {
|
||||||
|
const { totalComment, approvedComment } = props.singlePage.stats || {};
|
||||||
|
|
||||||
|
let text = t("core.page.list.fields.comments", {
|
||||||
|
comments: totalComment || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totalComment || !approvedComment) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingComments = totalComment - approvedComment;
|
||||||
|
|
||||||
|
if (pendingComments > 0) {
|
||||||
|
text += t("core.page.list.fields.comments-with-pending", {
|
||||||
|
count: pendingComments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -66,15 +94,20 @@ const externalUrl = computed(() => {
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs text-gray-500">
|
<span
|
||||||
{{
|
class="cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline"
|
||||||
$t("core.page.list.fields.comments", {
|
@click="commentListVisible = true"
|
||||||
comments: singlePage.stats.totalComment || 0,
|
>
|
||||||
})
|
{{ commentText }}
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SubjectQueryCommentListModal
|
||||||
|
v-if="commentListVisible"
|
||||||
|
:subject-ref-key="commentSubjectRefKey"
|
||||||
|
@close="commentListVisible = false"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { postLabels } from "@/constants/labels";
|
import { postLabels } from "@/constants/labels";
|
||||||
|
import SubjectQueryCommentListModal from "@console/modules/contents/comments/components/SubjectQueryCommentListModal.vue";
|
||||||
import type { ListedPost } from "@halo-dev/api-client";
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
IconExternalLinkLine,
|
IconExternalLinkLine,
|
||||||
|
@ -7,7 +8,8 @@ import {
|
||||||
VSpace,
|
VSpace,
|
||||||
VStatusDot,
|
VStatusDot,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import PostTag from "../../tags/components/PostTag.vue";
|
import PostTag from "../../tags/components/PostTag.vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
@ -17,6 +19,8 @@ const props = withDefaults(
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const externalUrl = computed(() => {
|
const externalUrl = computed(() => {
|
||||||
const { status, metadata } = props.post.post;
|
const { status, metadata } = props.post.post;
|
||||||
if (metadata.labels?.[postLabels.PUBLISHED] === "true") {
|
if (metadata.labels?.[postLabels.PUBLISHED] === "true") {
|
||||||
|
@ -24,6 +28,30 @@ const externalUrl = computed(() => {
|
||||||
}
|
}
|
||||||
return `/preview/posts/${metadata.name}`;
|
return `/preview/posts/${metadata.name}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const commentSubjectRefKey = `content.halo.run/Post/${props.post.post.metadata.name}`;
|
||||||
|
const commentListVisible = ref(false);
|
||||||
|
|
||||||
|
const commentText = computed(() => {
|
||||||
|
const { totalComment, approvedComment } = props.post.stats || {};
|
||||||
|
|
||||||
|
let text = t("core.post.list.fields.comments", {
|
||||||
|
comments: totalComment || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totalComment || !approvedComment) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingComments = totalComment - approvedComment;
|
||||||
|
|
||||||
|
if (pendingComments > 0) {
|
||||||
|
text += t("core.post.list.fields.comments-with-pending", {
|
||||||
|
count: pendingComments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -83,12 +111,11 @@ const externalUrl = computed(() => {
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs text-gray-500">
|
<span
|
||||||
{{
|
class="cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline"
|
||||||
$t("core.post.list.fields.comments", {
|
@click="commentListVisible = true"
|
||||||
comments: post.stats.totalComment || 0,
|
>
|
||||||
})
|
{{ commentText }}
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
<span v-if="post.post.spec.pinned" class="text-xs text-gray-500">
|
<span v-if="post.post.spec.pinned" class="text-xs text-gray-500">
|
||||||
{{ $t("core.post.list.fields.pinned") }}
|
{{ $t("core.post.list.fields.pinned") }}
|
||||||
|
@ -103,6 +130,12 @@ const externalUrl = computed(() => {
|
||||||
></PostTag>
|
></PostTag>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SubjectQueryCommentListModal
|
||||||
|
v-if="commentListVisible"
|
||||||
|
:subject-ref-key="commentSubjectRefKey"
|
||||||
|
@close="commentListVisible = false"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { postLabels } from "@/constants/labels";
|
import { postLabels } from "@/constants/labels";
|
||||||
import { formatDatetime, relativeTimeTo } from "@/utils/date";
|
import { formatDatetime, relativeTimeTo } from "@/utils/date";
|
||||||
|
import SubjectQueryCommentListModal from "@console/modules/contents/comments/components/SubjectQueryCommentListModal.vue";
|
||||||
import type { ListedPost } from "@halo-dev/api-client";
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
IconExternalLinkLine,
|
IconExternalLinkLine,
|
||||||
|
@ -9,7 +10,10 @@ import {
|
||||||
VSpace,
|
VSpace,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
post: ListedPost;
|
post: ListedPost;
|
||||||
|
@ -33,6 +37,33 @@ const datetime = computed(() => {
|
||||||
props.post.post.metadata.creationTimestamp
|
props.post.post.metadata.creationTimestamp
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const commentSubjectRefKey = `content.halo.run/Post/${props.post.post.metadata.name}`;
|
||||||
|
const commentListVisible = ref(false);
|
||||||
|
|
||||||
|
const commentText = computed(() => {
|
||||||
|
const { totalComment, approvedComment } = props.post.stats || {};
|
||||||
|
|
||||||
|
let text = t("core.dashboard.widgets.presets.recent_published.comments", {
|
||||||
|
comments: totalComment || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totalComment || !approvedComment) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingComments = totalComment - approvedComment;
|
||||||
|
|
||||||
|
if (pendingComments > 0) {
|
||||||
|
text += t(
|
||||||
|
"core.dashboard.widgets.presets.recent_published.comments-with-pending",
|
||||||
|
{
|
||||||
|
count: pendingComments,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -54,14 +85,18 @@ const datetime = computed(() => {
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs text-gray-500">
|
<span
|
||||||
{{
|
class="cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline"
|
||||||
$t("core.dashboard.widgets.presets.recent_published.comments", {
|
@click="commentListVisible = true"
|
||||||
comments: post.stats.totalComment || 0,
|
>
|
||||||
})
|
{{ commentText }}
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
|
<SubjectQueryCommentListModal
|
||||||
|
v-if="commentListVisible"
|
||||||
|
:subject-ref-key="commentSubjectRefKey"
|
||||||
|
@close="commentListVisible = false"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
|
|
|
@ -33,6 +33,7 @@ core:
|
||||||
recent_published:
|
recent_published:
|
||||||
empty:
|
empty:
|
||||||
title: No published posts
|
title: No published posts
|
||||||
|
comments-with-pending: " ({count} pending comments)"
|
||||||
notification:
|
notification:
|
||||||
title: Notifications
|
title: Notifications
|
||||||
empty:
|
empty:
|
||||||
|
@ -116,6 +117,7 @@ core:
|
||||||
fields:
|
fields:
|
||||||
schedule_publish:
|
schedule_publish:
|
||||||
tooltip: Schedule publish
|
tooltip: Schedule publish
|
||||||
|
comments-with-pending: ({count} pending comments)
|
||||||
settings:
|
settings:
|
||||||
fields:
|
fields:
|
||||||
publish_time:
|
publish_time:
|
||||||
|
@ -175,6 +177,10 @@ core:
|
||||||
fields:
|
fields:
|
||||||
prevent_parent_post_cascade_query: Prevent parent category from including this category and its subcategories in cascade post queries
|
prevent_parent_post_cascade_query: Prevent parent category from including this category and its subcategories in cascade post queries
|
||||||
hide_from_list: This category is hidden, This category and its subcategories, as well as its posts, will not be displayed in the front-end list. You need to actively visit the category archive page
|
hide_from_list: This category is hidden, This category and its subcategories, as well as its posts, will not be displayed in the front-end list. You need to actively visit the category archive page
|
||||||
|
page:
|
||||||
|
list:
|
||||||
|
fields:
|
||||||
|
comments-with-pending: " ({count} pending comments)"
|
||||||
page_editor:
|
page_editor:
|
||||||
actions:
|
actions:
|
||||||
snapshots: Snapshots
|
snapshots: Snapshots
|
||||||
|
@ -375,10 +381,16 @@ core:
|
||||||
operations:
|
operations:
|
||||||
enable:
|
enable:
|
||||||
title: Enable
|
title: Enable
|
||||||
description: Are you sure you want to enable this user?
|
description: Are you sure you want to enable this user? Once enabled, the user will be able to log back into the system.
|
||||||
|
enable_in_batch:
|
||||||
|
title: Enable
|
||||||
|
description: Are you sure you want to enable the selected users? Once enabled, these users will be able to log back into the system.
|
||||||
disable:
|
disable:
|
||||||
title: Disable
|
title: Disable
|
||||||
description: Are you sure you want to disable this user? This user will not be able to log in after being disabled
|
description: Are you sure you want to disable this user? Once disabled, the user will no longer be able to log in.
|
||||||
|
disable_in_batch:
|
||||||
|
title: Disable
|
||||||
|
description: Are you sure you want to disable the selected users? Once disabled, these users will no longer be able to log in.
|
||||||
grant_permission_modal:
|
grant_permission_modal:
|
||||||
roles_preview:
|
roles_preview:
|
||||||
all: The currently selected role contains all permissions
|
all: The currently selected role contains all permissions
|
||||||
|
|
|
@ -73,6 +73,7 @@ core:
|
||||||
comments: "{comments} Comments"
|
comments: "{comments} Comments"
|
||||||
empty:
|
empty:
|
||||||
title: No published posts
|
title: No published posts
|
||||||
|
comments-with-pending: " ({count} pending comments)"
|
||||||
notification:
|
notification:
|
||||||
title: Notifications
|
title: Notifications
|
||||||
empty:
|
empty:
|
||||||
|
@ -238,6 +239,7 @@ core:
|
||||||
pinned: Pinned
|
pinned: Pinned
|
||||||
schedule_publish:
|
schedule_publish:
|
||||||
tooltip: Schedule publish
|
tooltip: Schedule publish
|
||||||
|
comments-with-pending: " ({count} pending comments)"
|
||||||
settings:
|
settings:
|
||||||
title: Settings
|
title: Settings
|
||||||
groups:
|
groups:
|
||||||
|
@ -404,7 +406,10 @@ core:
|
||||||
help: Theme adaptation is required to support
|
help: Theme adaptation is required to support
|
||||||
description:
|
description:
|
||||||
label: Description
|
label: Description
|
||||||
help: "The description will be automatically added to the page's meta description tag for SEO; other display purposes require theme adaptation"
|
help: >-
|
||||||
|
The description will be automatically added to the page's meta
|
||||||
|
description tag for SEO; other display purposes require theme
|
||||||
|
adaptation
|
||||||
prevent_parent_post_cascade_query:
|
prevent_parent_post_cascade_query:
|
||||||
label: Prevent Parent Post Cascade Query
|
label: Prevent Parent Post Cascade Query
|
||||||
help: >-
|
help: >-
|
||||||
|
@ -473,6 +478,7 @@ core:
|
||||||
fields:
|
fields:
|
||||||
visits: "{visits} Visits"
|
visits: "{visits} Visits"
|
||||||
comments: "{comments} Comments"
|
comments: "{comments} Comments"
|
||||||
|
comments-with-pending: " ({count} pending comments)"
|
||||||
settings:
|
settings:
|
||||||
title: Settings
|
title: Settings
|
||||||
groups:
|
groups:
|
||||||
|
@ -1143,19 +1149,23 @@ core:
|
||||||
enable:
|
enable:
|
||||||
title: Enable
|
title: Enable
|
||||||
description: >-
|
description: >-
|
||||||
Are you sure you want to enable this user? Once enabled, the user will be able to log back into the system.
|
Are you sure you want to enable this user? Once enabled, the user will
|
||||||
|
be able to log back into the system.
|
||||||
enable_in_batch:
|
enable_in_batch:
|
||||||
title: Enable
|
title: Enable
|
||||||
description: >-
|
description: >-
|
||||||
Are you sure you want to enable the selected users? Once enabled, these users will be able to log back into the system.
|
Are you sure you want to enable the selected users? Once enabled,
|
||||||
|
these users will be able to log back into the system.
|
||||||
disable:
|
disable:
|
||||||
title: Disable
|
title: Disable
|
||||||
description: >-
|
description: >-
|
||||||
Are you sure you want to disable this user? Once disabled, the user will no longer be able to log in.
|
Are you sure you want to disable this user? Once disabled, the user
|
||||||
|
will no longer be able to log in.
|
||||||
disable_in_batch:
|
disable_in_batch:
|
||||||
title: Disable
|
title: Disable
|
||||||
description: >-
|
description: >-
|
||||||
Are you sure you want to disable the selected users? Once disabled, these users will no longer be able to log in.
|
Are you sure you want to disable the selected users? Once disabled,
|
||||||
|
these users will no longer be able to log in.
|
||||||
filters:
|
filters:
|
||||||
role:
|
role:
|
||||||
label: Role
|
label: Role
|
||||||
|
|
|
@ -67,6 +67,7 @@ core:
|
||||||
comments: 评论 {comments}
|
comments: 评论 {comments}
|
||||||
empty:
|
empty:
|
||||||
title: 暂无已发布文章
|
title: 暂无已发布文章
|
||||||
|
comments-with-pending: " ({count} 条待审核)"
|
||||||
notification:
|
notification:
|
||||||
title: 通知
|
title: 通知
|
||||||
empty:
|
empty:
|
||||||
|
@ -224,6 +225,7 @@ core:
|
||||||
pinned: 已置顶
|
pinned: 已置顶
|
||||||
schedule_publish:
|
schedule_publish:
|
||||||
tooltip: 定时发布
|
tooltip: 定时发布
|
||||||
|
comments-with-pending: ({count} 条待审核)
|
||||||
settings:
|
settings:
|
||||||
title: 文章设置
|
title: 文章设置
|
||||||
groups:
|
groups:
|
||||||
|
@ -451,6 +453,7 @@ core:
|
||||||
fields:
|
fields:
|
||||||
visits: 访问量 {visits}
|
visits: 访问量 {visits}
|
||||||
comments: 评论 {comments}
|
comments: 评论 {comments}
|
||||||
|
comments-with-pending: ({count} 条待审核)
|
||||||
settings:
|
settings:
|
||||||
title: 页面设置
|
title: 页面设置
|
||||||
groups:
|
groups:
|
||||||
|
|
|
@ -67,6 +67,7 @@ core:
|
||||||
comments: 留言 {comments}
|
comments: 留言 {comments}
|
||||||
empty:
|
empty:
|
||||||
title: 沒有已發布文章
|
title: 沒有已發布文章
|
||||||
|
comments-with-pending: " ({count} 條待審核)"
|
||||||
notification:
|
notification:
|
||||||
title: 通知
|
title: 通知
|
||||||
empty:
|
empty:
|
||||||
|
@ -224,6 +225,7 @@ core:
|
||||||
pinned: 已置頂
|
pinned: 已置頂
|
||||||
schedule_publish:
|
schedule_publish:
|
||||||
tooltip: 定時發佈
|
tooltip: 定時發佈
|
||||||
|
comments-with-pending: ({count} 條待審核)
|
||||||
settings:
|
settings:
|
||||||
title: 文章設置
|
title: 文章設置
|
||||||
groups:
|
groups:
|
||||||
|
@ -436,6 +438,7 @@ core:
|
||||||
fields:
|
fields:
|
||||||
visits: 訪問量 {visits}
|
visits: 訪問量 {visits}
|
||||||
comments: 留言 {comments}
|
comments: 留言 {comments}
|
||||||
|
comments-with-pending: " ({count} 條待審核)"
|
||||||
settings:
|
settings:
|
||||||
title: 頁面設置
|
title: 頁面設置
|
||||||
groups:
|
groups:
|
||||||
|
|
Loading…
Reference in New Issue