refactor: simplify the code of reply creation modal (#5972)

#### What type of PR is this?

/area ui
/kind improvement
/milestone 2.16.x

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

简化评论回复组件的代码。

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

```release-note
None 
```
pull/5975/head
Ryan Wang 2024-05-23 10:56:50 +08:00 committed by GitHub
parent 89b20bf5a3
commit 99eae2f31b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 88 additions and 134 deletions

View File

@ -1,18 +1,18 @@
<script lang="ts" setup>
import {
Dialog,
VAvatar,
VButton,
VEntity,
VEntityField,
VStatusDot,
VSpace,
VEmpty,
IconAddCircle,
IconExternalLinkLine,
VLoading,
Toast,
VAvatar,
VButton,
VDropdownItem,
VEmpty,
VEntity,
VEntityField,
VLoading,
VSpace,
VStatusDot,
VTag,
} from "@halo-dev/components";
import ReplyCreationModal from "./ReplyCreationModal.vue";
@ -24,7 +24,7 @@ import type {
SinglePage,
} from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date";
import { computed, provide, ref, onMounted, type Ref } from "vue";
import { computed, onMounted, provide, ref, type Ref } from "vue";
import ReplyListItem from "./ReplyListItem.vue";
import { apiClient } from "@/utils/api-client";
import { cloneDeep } from "lodash-es";
@ -33,9 +33,9 @@ import { useQuery, useQueryClient } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
import { usePluginModuleStore } from "@/stores/plugin";
import type {
PluginModule,
CommentSubjectRefProvider,
CommentSubjectRefResult,
PluginModule,
} from "@halo-dev/console-shared";
const { currentUserHasPermission } = usePermission();
@ -48,12 +48,10 @@ const props = withDefaults(
isSelected?: boolean;
}>(),
{
comment: undefined,
isSelected: false,
}
);
const selectedReply = ref<ListedReply>();
const hoveredReply = ref<ListedReply>();
const showReplies = ref(false);
const replyModal = ref(false);
@ -182,23 +180,14 @@ const handleToggleShowReplies = async () => {
}
};
const handleTriggerReply = () => {
replyModal.value = true;
};
const onTriggerReply = (reply: ListedReply) => {
selectedReply.value = reply;
replyModal.value = true;
};
const onReplyCreationModalClose = () => {
selectedReply.value = undefined;
queryClient.invalidateQueries({ queryKey: ["comments"] });
if (showReplies.value) {
refetch();
}
replyModal.value = false;
};
// Subject ref processing
@ -287,10 +276,8 @@ const subjectRefResult = computed(() => {
<template>
<ReplyCreationModal
:key="comment?.comment.metadata.name"
v-model:visible="replyModal"
v-if="replyModal"
:comment="comment"
:reply="selectedReply"
@close="onReplyCreationModalClose"
/>
<VEntity :is-selected="isSelected" :class="{ 'hover:bg-white': showReplies }">
@ -364,7 +351,7 @@ const subjectRefResult = computed(() => {
/>
<span
class="select-none text-gray-700 hover:text-gray-900"
@click="handleTriggerReply"
@click="replyModal = true"
>
{{ $t("core.comment.operations.reply.button") }}
</span>
@ -458,8 +445,8 @@ const subjectRefResult = computed(() => {
:key="reply.reply.metadata.name"
:class="{ 'hover:bg-white': showReplies }"
:reply="reply"
:comment="comment"
:replies="replies"
@reply="onTriggerReply"
></ReplyListItem>
</div>
</Transition>

View File

@ -1,11 +1,11 @@
<script lang="ts" setup>
import {
VModal,
VSpace,
VButton,
IconMotionLine,
Toast,
VButton,
VDropdown,
VModal,
VSpace,
} from "@halo-dev/components";
import SubmitButton from "@/components/button/SubmitButton.vue";
import type {
@ -16,9 +16,7 @@ import type {
// @ts-ignore
import { Picker } from "emoji-mart";
import i18n from "@emoji-mart/data/i18n/zh.json";
import { computed, nextTick, ref, watch, watchEffect } from "vue";
import { reset } from "@formkit/core";
import { cloneDeep } from "lodash-es";
import { onMounted, ref } from "vue";
import { setFocus } from "@/formkit/utils/focus";
import { apiClient } from "@/utils/api-client";
import { useI18n } from "vue-i18n";
@ -27,7 +25,6 @@ const { t } = useI18n();
const props = withDefaults(
defineProps<{
visible?: boolean;
comment?: ListedComment;
reply?: ListedReply;
}>(),
@ -39,76 +36,38 @@ const props = withDefaults(
);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
}>();
const initialFormState: ReplyRequest = {
const modal = ref();
const formState = ref<ReplyRequest>({
raw: "",
content: "",
allowNotification: true,
quoteReply: undefined,
};
const formState = ref<ReplyRequest>(cloneDeep(initialFormState));
});
const saving = ref(false);
watch(
() => formState.value.raw,
(newValue) => {
formState.value.content = newValue;
}
);
const formId = computed(() => {
return `comment-reply-form-${[
props.comment?.comment.metadata.name,
props.reply?.reply.metadata.name,
].join("-")}`;
onMounted(() => {
setFocus("content-input");
});
const contentInputId = computed(() => {
return `content-input-${[
props.comment?.comment.metadata.name,
props.reply?.reply.metadata.name,
].join("-")}`;
});
const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
emit("close");
}
};
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
reset(formId.value);
};
watch(
() => props.visible,
async (visible) => {
if (visible) {
await nextTick();
setFocus(contentInputId.value);
} else {
handleResetForm();
}
}
);
const handleCreateReply = async () => {
try {
saving.value = true;
if (props.reply) {
formState.value.quoteReply = props.reply.reply.metadata.name;
}
formState.value.content = formState.value.raw;
await apiClient.comment.createReply({
name: props.comment?.comment.metadata.name as string,
replyRequest: formState.value,
});
onVisibleChange(false);
modal.value.close();
Toast.success(
t("core.comment.reply_modal.operations.submit.toast_success")
@ -123,57 +82,56 @@ const handleCreateReply = async () => {
// Emoji picker
const emojiPickerRef = ref<HTMLElement | null>(null);
const handleCreateEmojiPicker = () => {
const handleCreateEmojiPicker = async () => {
if (emojiPickerRef.value?.childElementCount) {
return;
}
const data = await import("@emoji-mart/data");
const emojiPicker = new Picker({
data: async () => {
const data = await import("@emoji-mart/data");
return Object.assign({}, data);
},
data: Object.assign({}, data),
theme: "light",
autoFocus: true,
i18n: i18n,
onEmojiSelect: onEmojiSelect,
});
emojiPickerRef.value?.appendChild(emojiPicker as unknown as Node);
};
const onEmojiSelect = (emoji: { native: string }) => {
formState.value.raw += emoji.native;
setFocus(contentInputId.value);
setFocus("content-input");
};
watchEffect(() => {
if (emojiPickerRef.value) {
handleCreateEmojiPicker();
}
});
</script>
<template>
<VModal
ref="modal"
:title="$t('core.comment.reply_modal.title')"
:visible="visible"
:width="500"
@update:visible="onVisibleChange"
@close="emit('close')"
>
<FormKit
:id="formId"
:name="formId"
id="create-reply-form"
name="create-reply-form"
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleCreateReply"
>
<FormKit
:id="contentInputId"
id="content-input"
v-model="formState.raw"
type="textarea"
:validation-label="$t('core.comment.reply_modal.fields.content.label')"
:rows="6"
value=""
validation="required|length:0,1024"
></FormKit>
</FormKit>
<div class="mt-2 flex justify-end">
<VDropdown :classes="['!p-0']">
<VDropdown :classes="['!p-0']" @show="handleCreateEmojiPicker">
<IconMotionLine
class="h-5 w-5 cursor-pointer text-gray-500 transition-all hover:text-gray-900"
/>
@ -185,14 +143,13 @@ watchEffect(() => {
<template #footer>
<VSpace>
<SubmitButton
v-if="visible"
:loading="saving"
type="secondary"
:text="$t('core.common.buttons.submit')"
@submit="$formkit.submit(formId)"
@submit="$formkit.submit('create-reply-form')"
>
</SubmitButton>
<VButton @click="onVisibleChange(false)">
<VButton @click="modal.close()">
{{ $t("core.common.buttons.cancel_and_shortcut") }}
</VButton>
</VSpace>

View File

@ -1,28 +1,30 @@
<script lang="ts" setup>
import {
VAvatar,
VTag,
VEntityField,
VEntity,
Dialog,
VStatusDot,
VDropdownItem,
IconReplyLine,
Toast,
VAvatar,
VDropdownItem,
VEntity,
VEntityField,
VStatusDot,
VTag,
} from "@halo-dev/components";
import type { ListedReply } from "@halo-dev/api-client";
import type { ListedComment, ListedReply } from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date";
import { apiClient } from "@/utils/api-client";
import { computed, inject, type Ref } from "vue";
import { computed, inject, ref, type Ref } from "vue";
import { cloneDeep } from "lodash-es";
import { useI18n } from "vue-i18n";
import { useQueryClient } from "@tanstack/vue-query";
import ReplyCreationModal from "./ReplyCreationModal.vue";
const { t } = useI18n();
const queryClient = useQueryClient();
const props = withDefaults(
defineProps<{
comment: ListedComment;
reply: ListedReply;
replies?: ListedReply[];
}>(),
@ -32,10 +34,6 @@ const props = withDefaults(
}
);
const emit = defineEmits<{
(event: "reply", reply: ListedReply): void;
}>();
const quoteReply = computed(() => {
const { quoteReply: replyName } = props.reply.reply.spec;
@ -90,10 +88,6 @@ const handleApprove = async () => {
}
};
const handleTriggerReply = () => {
emit("reply", props.reply);
};
// Show hovered reply
const hoveredReply = inject<Ref<ListedReply | undefined>>("hoveredReply");
@ -108,9 +102,25 @@ const isHoveredReply = computed(() => {
hoveredReply?.value?.reply.metadata.name === props.reply.reply.metadata.name
);
});
// Create reply
const replyModal = ref(false);
function onReplyCreationModalClose() {
queryClient.invalidateQueries({
queryKey: ["comment-replies", props.comment.comment.metadata.name],
});
replyModal.value = false;
}
</script>
<template>
<ReplyCreationModal
v-if="replyModal"
:comment="comment"
:reply="reply"
@close="onReplyCreationModalClose"
/>
<VEntity class="!px-0 !py-2" :class="{ 'animate-breath': isHoveredReply }">
<template #start>
<VEntityField>
@ -150,7 +160,7 @@ const isHoveredReply = computed(() => {
<div class="flex items-center gap-3 text-xs">
<span
class="select-none text-gray-700 hover:text-gray-900"
@click="handleTriggerReply"
@click="replyModal = true"
>
{{ $t("core.comment.operations.reply.button") }}
</span>

View File

@ -47,7 +47,7 @@
"@codemirror/legacy-modes": "^6.3.0",
"@codemirror/state": "^6.1.4",
"@codemirror/view": "^6.5.1",
"@emoji-mart/data": "^1.0.8",
"@emoji-mart/data": "^1.2.1",
"@formkit/core": "^1.5.9",
"@formkit/i18n": "^1.5.9",
"@formkit/inputs": "^1.5.9",
@ -81,7 +81,7 @@
"colorjs.io": "^0.4.3",
"cropperjs": "^1.5.13",
"dayjs": "^1.11.7",
"emoji-mart": "^5.3.3",
"emoji-mart": "^5.6.0",
"floating-vue": "^5.2.2",
"fuse.js": "^6.6.2",
"jsencrypt": "^3.3.2",

View File

@ -36,8 +36,8 @@ importers:
specifier: ^6.5.1
version: 6.5.1
'@emoji-mart/data':
specifier: ^1.0.8
version: 1.0.8
specifier: ^1.2.1
version: 1.2.1
'@formkit/core':
specifier: ^1.5.9
version: 1.5.9
@ -138,8 +138,8 @@ importers:
specifier: ^1.11.7
version: 1.11.7
emoji-mart:
specifier: ^5.3.3
version: 5.3.3
specifier: ^5.6.0
version: 5.6.0
floating-vue:
specifier: ^5.2.2
version: 5.2.2(vue@3.4.27(typescript@5.3.3))
@ -1923,8 +1923,8 @@ packages:
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
'@emoji-mart/data@1.0.8':
resolution: {integrity: sha512-AMpqLrR80dHfj8ZA6xaf8/t9reBy88vz07fBdwKVVoUX6X2Wi+R2p2uhIZFqNZe8zRd6kga3hKheqK+deElEZw==}
'@emoji-mart/data@1.2.1':
resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==}
'@emotion/use-insertion-effect-with-fallbacks@1.0.1':
resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==}
@ -5721,8 +5721,8 @@ packages:
element-resize-detector@1.2.4:
resolution: {integrity: sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==}
emoji-mart@5.3.3:
resolution: {integrity: sha512-rr3wXUimYFQ5Mf50P/5UOsRibr5JSJE3Nj4zw0aDglb3GSHzn/wGKBoXoSkjtWaji8UcmXcYn3cdilD2Eix6iQ==}
emoji-mart@5.6.0:
resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==}
emoji-regex@10.3.0:
resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
@ -12690,7 +12690,7 @@ snapshots:
'@discoveryjs/json-ext@0.5.7': {}
'@emoji-mart/data@1.0.8': {}
'@emoji-mart/data@1.2.1': {}
'@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0)':
dependencies:
@ -17198,7 +17198,7 @@ snapshots:
dependencies:
batch-processor: 1.0.0
emoji-mart@5.3.3: {}
emoji-mart@5.6.0: {}
emoji-regex@10.3.0: {}