mirror of https://github.com/halo-dev/halo
feat: add support for setting an owner for posts (#6178)
#### What type of PR is this? /area ui /kind feature /milestone 2.17.x #### What this PR does / why we need it: 支持手动为文章设置作者。 <img width="734" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/b91b1754-4f50-4333-8478-6735d5846847"> #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/5720 #### Special notes for your reviewer: 需要测试: 1. 打开任意文章的设置,在高级中设置作者并保存,观察是否成功。 2. 选择一批文章,点击批量设置,设置一个作者,观察是否设置成功。 #### Does this PR introduce a user-facing change? ```release-note 支持手动为文章设置作者。 ```pull/6185/head
parent
9d478eecf9
commit
bb0a3bc467
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import PostContributorList from "@/components/user/PostContributorList.vue";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import type { ListedSinglePage, SinglePage } from "@halo-dev/api-client";
|
import type { ListedSinglePage, SinglePage } from "@halo-dev/api-client";
|
||||||
|
@ -25,7 +26,6 @@ import { useQuery } from "@tanstack/vue-query";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import ContributorList from "../_components/ContributorList.vue";
|
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -327,7 +327,10 @@ watch(
|
||||||
<template #end>
|
<template #end>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<ContributorList :contributors="singlePage.contributors" />
|
<PostContributorList
|
||||||
|
:owner="singlePage.owner"
|
||||||
|
:contributors="singlePage.contributors"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
<VEntityField v-if="!singlePage?.page?.spec.deleted">
|
<VEntityField v-if="!singlePage?.page?.spec.deleted">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import PostContributorList from "@/components/user/PostContributorList.vue";
|
||||||
import { singlePageLabels } from "@/constants/labels";
|
import { singlePageLabels } from "@/constants/labels";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
|
@ -23,7 +24,6 @@ import type { Ref } from "vue";
|
||||||
import { computed, inject, ref } from "vue";
|
import { computed, inject, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { RouterLink } from "vue-router";
|
import { RouterLink } from "vue-router";
|
||||||
import ContributorList from "../../_components/ContributorList.vue";
|
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -184,7 +184,10 @@ const handleDelete = async () => {
|
||||||
<template #end>
|
<template #end>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<ContributorList :contributors="singlePage.contributors" />
|
<PostContributorList
|
||||||
|
:owner="singlePage.owner"
|
||||||
|
:contributors="singlePage.contributors"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
<VEntityField :description="publishStatus">
|
<VEntityField :description="publishStatus">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import PostContributorList from "@/components/user/PostContributorList.vue";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import type { ListedPost, Post } from "@halo-dev/api-client";
|
import type { ListedPost, Post } from "@halo-dev/api-client";
|
||||||
|
@ -25,7 +26,6 @@ import { useQuery } from "@tanstack/vue-query";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import ContributorList from "../_components/ContributorList.vue";
|
|
||||||
import PostTag from "./tags/components/PostTag.vue";
|
import PostTag from "./tags/components/PostTag.vue";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
@ -351,7 +351,10 @@ watch(
|
||||||
<template #end>
|
<template #end>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<ContributorList :contributors="post.contributors" />
|
<PostContributorList
|
||||||
|
:owner="post.owner"
|
||||||
|
:contributors="post.contributors"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
<VEntityField v-if="!post?.post?.spec.deleted">
|
<VEntityField v-if="!post?.post?.spec.deleted">
|
||||||
|
|
|
@ -22,6 +22,10 @@ interface FormData {
|
||||||
names?: string[];
|
names?: string[];
|
||||||
op: ArrayPatchOp;
|
op: ArrayPatchOp;
|
||||||
};
|
};
|
||||||
|
owner: {
|
||||||
|
enabled: boolean;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
visible: {
|
visible: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
value: "PUBLIC" | "PRIVATE";
|
value: "PUBLIC" | "PRIVATE";
|
||||||
|
@ -73,6 +77,14 @@ const { mutate, isLoading } = useMutation({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.owner.enabled) {
|
||||||
|
jsonPatchInner.push({
|
||||||
|
op: "add",
|
||||||
|
path: "/spec/owner",
|
||||||
|
value: data.owner.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.visible.enabled) {
|
if (data.visible.enabled) {
|
||||||
jsonPatchInner.push({
|
jsonPatchInner.push({
|
||||||
op: "add",
|
op: "add",
|
||||||
|
@ -235,6 +247,25 @@ function onSubmit(data: FormData) {
|
||||||
validation="required"
|
validation="required"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
|
<FormKit
|
||||||
|
v-slot="{ value }"
|
||||||
|
type="group"
|
||||||
|
name="owner"
|
||||||
|
:label="$t('core.post.batch_setting_modal.fields.owner_group')"
|
||||||
|
>
|
||||||
|
<FormKit
|
||||||
|
:value="false"
|
||||||
|
:label="$t('core.post.batch_setting_modal.fields.common.enabled')"
|
||||||
|
type="checkbox"
|
||||||
|
name="enabled"
|
||||||
|
></FormKit>
|
||||||
|
<FormKit
|
||||||
|
v-if="value?.enabled"
|
||||||
|
:label="$t('core.post.batch_setting_modal.fields.owner_value')"
|
||||||
|
name="value"
|
||||||
|
type="userSelect"
|
||||||
|
></FormKit>
|
||||||
|
</FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-slot="{ value }"
|
v-slot="{ value }"
|
||||||
type="group"
|
type="group"
|
||||||
|
|
|
@ -391,6 +391,11 @@ const showCancelPublishButton = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||||
|
<FormKit
|
||||||
|
v-model="formState.spec.owner"
|
||||||
|
:label="$t('core.post.settings.fields.owner.label')"
|
||||||
|
type="userSelect"
|
||||||
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.allowComment"
|
v-model="formState.spec.allowComment"
|
||||||
:options="[
|
:options="[
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ContributorList from "@console/modules/contents/_components/ContributorList.vue";
|
import PostContributorList from "@/components/user/PostContributorList.vue";
|
||||||
import type { ListedPost } from "@halo-dev/api-client";
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
import { VEntityField } from "@halo-dev/components";
|
import { VEntityField } from "@halo-dev/components";
|
||||||
|
|
||||||
|
@ -14,7 +14,10 @@ withDefaults(
|
||||||
<template>
|
<template>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<ContributorList :contributors="post.contributors" />
|
<PostContributorList
|
||||||
|
:owner="post.owner"
|
||||||
|
:contributors="post.contributors"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
import type { CoreMenuGroupId } from "@halo-dev/console-shared";
|
|
||||||
import type { FormKitInputs } from "@formkit/inputs";
|
import type { FormKitInputs } from "@formkit/inputs";
|
||||||
|
import type { CoreMenuGroupId } from "@halo-dev/console-shared";
|
||||||
|
|
||||||
import "vue-router";
|
import "vue-router";
|
||||||
|
|
||||||
|
@ -124,5 +124,10 @@ declare module "@formkit/inputs" {
|
||||||
type: "code";
|
type: "code";
|
||||||
value?: string;
|
value?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
userSelect: {
|
||||||
|
type: "userSelect";
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,35 @@
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import type { Contributor } from "@halo-dev/api-client";
|
import type { Contributor } from "@halo-dev/api-client";
|
||||||
import { VAvatar, VAvatarGroup } from "@halo-dev/components";
|
import { VAvatar, VAvatarGroup } from "@halo-dev/components";
|
||||||
|
import { computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
allowViewUserDetail?: boolean;
|
||||||
|
owner?: Contributor;
|
||||||
contributors: Contributor[];
|
contributors: Contributor[];
|
||||||
}>(),
|
}>(),
|
||||||
{}
|
{
|
||||||
|
allowViewUserDetail: true,
|
||||||
|
owner: undefined,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
const contributorsWithoutOwner = computed(() =>
|
||||||
|
props.contributors.filter(
|
||||||
|
(contributor) => contributor.name !== props.owner?.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
function handleRouteToUserDetail(contributor: Contributor) {
|
function handleRouteToUserDetail(contributor: Contributor) {
|
||||||
if (!currentUserHasPermission(["system:users:view"])) {
|
if (
|
||||||
|
!currentUserHasPermission(["system:users:view"]) ||
|
||||||
|
!props.allowViewUserDetail
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push({
|
router.push({
|
||||||
|
@ -28,7 +43,14 @@ function handleRouteToUserDetail(contributor: Contributor) {
|
||||||
<template>
|
<template>
|
||||||
<VAvatarGroup size="xs" circle>
|
<VAvatarGroup size="xs" circle>
|
||||||
<VAvatar
|
<VAvatar
|
||||||
v-for="contributor in contributors"
|
v-if="owner"
|
||||||
|
v-tooltip="owner?.displayName"
|
||||||
|
:src="owner.avatar"
|
||||||
|
:alt="owner.displayName"
|
||||||
|
@click="handleRouteToUserDetail(owner)"
|
||||||
|
/>
|
||||||
|
<VAvatar
|
||||||
|
v-for="contributor in contributorsWithoutOwner"
|
||||||
:key="contributor.name"
|
:key="contributor.name"
|
||||||
v-tooltip="contributor.displayName"
|
v-tooltip="contributor.displayName"
|
||||||
:src="contributor.avatar"
|
:src="contributor.avatar"
|
|
@ -25,6 +25,7 @@ import { tagSelect } from "./inputs/tag-select";
|
||||||
import { verificationForm } from "./inputs/verify-form";
|
import { verificationForm } from "./inputs/verify-form";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
|
|
||||||
|
import { userSelect } from "./inputs/user-select";
|
||||||
import autoScrollToErrors from "./plugins/auto-scroll-to-errors";
|
import autoScrollToErrors from "./plugins/auto-scroll-to-errors";
|
||||||
import passwordPreventAutocomplete from "./plugins/password-prevent-autocomplete";
|
import passwordPreventAutocomplete from "./plugins/password-prevent-autocomplete";
|
||||||
import radioAlt from "./plugins/radio-alt";
|
import radioAlt from "./plugins/radio-alt";
|
||||||
|
@ -65,6 +66,7 @@ const config: DefaultConfigOptions = {
|
||||||
tagCheckbox,
|
tagCheckbox,
|
||||||
tagSelect,
|
tagSelect,
|
||||||
verificationForm,
|
verificationForm,
|
||||||
|
userSelect,
|
||||||
},
|
},
|
||||||
locales: { zh, en },
|
locales: { zh, en },
|
||||||
locale: "zh",
|
locale: "zh",
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
// TODO: This is a temporary approach.
|
||||||
|
// We will provide searchable user selection components in the future.
|
||||||
|
|
||||||
|
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
|
||||||
|
import { defaultIcon, select, selects } from "@formkit/inputs";
|
||||||
|
import { coreApiClient } from "@halo-dev/api-client";
|
||||||
|
|
||||||
|
function optionsHandler(node: FormKitNode) {
|
||||||
|
node.on("created", async () => {
|
||||||
|
const { data } = await coreApiClient.user.listUser();
|
||||||
|
|
||||||
|
node.props.options = data.items.map((user) => {
|
||||||
|
return {
|
||||||
|
value: user.metadata.name,
|
||||||
|
label: user.spec.displayName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userSelect: FormKitTypeDefinition = {
|
||||||
|
...select,
|
||||||
|
props: ["placeholder"],
|
||||||
|
forceTypeProp: "select",
|
||||||
|
features: [optionsHandler, selects, defaultIcon("select", "select")],
|
||||||
|
};
|
|
@ -279,6 +279,8 @@ core:
|
||||||
label: Template
|
label: Template
|
||||||
cover:
|
cover:
|
||||||
label: Cover
|
label: Cover
|
||||||
|
owner:
|
||||||
|
label: Owner
|
||||||
tag:
|
tag:
|
||||||
filters:
|
filters:
|
||||||
sort:
|
sort:
|
||||||
|
@ -306,6 +308,8 @@ core:
|
||||||
visible_value: "Select visible option "
|
visible_value: "Select visible option "
|
||||||
allow_comment_group: " Allow comment"
|
allow_comment_group: " Allow comment"
|
||||||
allow_comment_value: Choose whether to allow comments
|
allow_comment_value: Choose whether to allow comments
|
||||||
|
owner_group: Owner
|
||||||
|
owner_value: Select owner
|
||||||
deleted_post:
|
deleted_post:
|
||||||
title: Deleted Posts
|
title: Deleted Posts
|
||||||
empty:
|
empty:
|
||||||
|
|
|
@ -267,6 +267,8 @@ core:
|
||||||
label: 自定义模板
|
label: 自定义模板
|
||||||
cover:
|
cover:
|
||||||
label: 封面图
|
label: 封面图
|
||||||
|
owner:
|
||||||
|
label: 作者
|
||||||
tag:
|
tag:
|
||||||
filters:
|
filters:
|
||||||
sort:
|
sort:
|
||||||
|
@ -294,6 +296,8 @@ core:
|
||||||
visible_value: 选择可见性
|
visible_value: 选择可见性
|
||||||
allow_comment_group: 允许评论
|
allow_comment_group: 允许评论
|
||||||
allow_comment_value: 选择是否允许评论
|
allow_comment_value: 选择是否允许评论
|
||||||
|
owner_group: 作者
|
||||||
|
owner_value: 设置作者
|
||||||
deleted_post:
|
deleted_post:
|
||||||
title: 文章回收站
|
title: 文章回收站
|
||||||
empty:
|
empty:
|
||||||
|
|
|
@ -267,6 +267,8 @@ core:
|
||||||
label: 自定義模板
|
label: 自定義模板
|
||||||
cover:
|
cover:
|
||||||
label: 封面圖
|
label: 封面圖
|
||||||
|
owner:
|
||||||
|
label: 作者
|
||||||
batch_setting_modal:
|
batch_setting_modal:
|
||||||
title: 文章批量設置
|
title: 文章批量設置
|
||||||
fields:
|
fields:
|
||||||
|
@ -286,6 +288,8 @@ core:
|
||||||
visible_value: 選擇可見性
|
visible_value: 選擇可見性
|
||||||
allow_comment_group: 允許評論
|
allow_comment_group: 允許評論
|
||||||
allow_comment_value: 選擇是否允許評論
|
allow_comment_value: 選擇是否允許評論
|
||||||
|
owner_group: 作者
|
||||||
|
owner_value: 設置作者
|
||||||
deleted_post:
|
deleted_post:
|
||||||
title: 文章回收站
|
title: 文章回收站
|
||||||
empty:
|
empty:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
|
import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
|
||||||
import HasPermission from "@/components/permission/HasPermission.vue";
|
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||||
|
import PostContributorList from "@/components/user/PostContributorList.vue";
|
||||||
import { postLabels } from "@/constants/labels";
|
import { postLabels } from "@/constants/labels";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import PostTag from "@console/modules/contents/posts/tags/components/PostTag.vue";
|
import PostTag from "@console/modules/contents/posts/tags/components/PostTag.vue";
|
||||||
|
@ -13,8 +14,6 @@ import {
|
||||||
IconEyeOff,
|
IconEyeOff,
|
||||||
IconTimerLine,
|
IconTimerLine,
|
||||||
Toast,
|
Toast,
|
||||||
VAvatar,
|
|
||||||
VAvatarGroup,
|
|
||||||
VDropdownDivider,
|
VDropdownDivider,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
VEntity,
|
VEntity,
|
||||||
|
@ -180,15 +179,11 @@ function handleUnpublish() {
|
||||||
<template #end>
|
<template #end>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<VAvatarGroup size="xs" circle>
|
<PostContributorList
|
||||||
<VAvatar
|
:owner="post.owner"
|
||||||
v-for="{ name, avatar, displayName } in post.contributors"
|
:contributors="post.contributors"
|
||||||
:key="name"
|
:allow-view-user-detail="false"
|
||||||
v-tooltip="displayName"
|
/>
|
||||||
:src="avatar"
|
|
||||||
:alt="displayName"
|
|
||||||
></VAvatar>
|
|
||||||
</VAvatarGroup>
|
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
<VEntityField :description="publishStatus">
|
<VEntityField :description="publishStatus">
|
||||||
|
|
Loading…
Reference in New Issue