mirror of https://github.com/halo-dev/halo
feat: add delete function for user notification (#4906)
#### What type of PR is this? /kind feature /area core /area console #### What this PR does / why we need it: 新增用户站内消息删除功能 <img width="588" alt="图片" src="https://github.com/halo-dev/halo/assets/21301288/6034e43c-0dbc-4e4e-88c6-4848c8b25e0c"> #### Which issue(s) this PR fixes: Fixes #4706 #### Does this PR introduce a user-facing change? ```release-note 新增用户站内消息删除功能 ```pull/4878/head^2
parent
6d6b1611d8
commit
91affebdd1
|
@ -10,6 +10,7 @@ import reactor.core.publisher.Mono;
|
|||
import run.halo.app.core.extension.notification.Notification;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.exception.AccessDeniedException;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link UserNotificationService}.
|
||||
|
@ -49,6 +50,19 @@ public class DefaultNotificationService implements UserNotificationService {
|
|||
.map(notification -> notification.getMetadata().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Notification> deleteByName(String username, String name) {
|
||||
return client.get(Notification.class, name)
|
||||
.doOnNext(notification -> {
|
||||
var recipient = notification.getSpec().getRecipient();
|
||||
if (!username.equals(recipient)) {
|
||||
throw new AccessDeniedException(
|
||||
"You have no permission to delete this notification.");
|
||||
}
|
||||
})
|
||||
.flatMap(client::delete);
|
||||
}
|
||||
|
||||
static boolean isRecipient(Notification notification, String username) {
|
||||
Assert.notNull(notification, "Notification must not be null");
|
||||
Assert.notNull(username, "Username must not be null");
|
||||
|
|
|
@ -37,4 +37,6 @@ public interface UserNotificationService {
|
|||
* @return the names of read notification that has been marked as read
|
||||
*/
|
||||
Flux<String> markSpecifiedAsRead(String username, List<String> names);
|
||||
|
||||
Mono<Notification> deleteByName(String username, String name);
|
||||
}
|
||||
|
|
|
@ -106,9 +106,34 @@ public class UserNotificationEndpoint implements CustomEndpoint {
|
|||
)
|
||||
.response(responseBuilder().implementationArray(String.class))
|
||||
)
|
||||
.DELETE("/notifications/{name}", this::deleteNotification,
|
||||
builder -> builder.operationId("DeleteSpecifiedNotification")
|
||||
.description("Delete the specified notification.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.PATH)
|
||||
.name("username")
|
||||
.description("Username")
|
||||
.required(true)
|
||||
)
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.PATH)
|
||||
.name("name")
|
||||
.description("Notification name")
|
||||
.required(true)
|
||||
)
|
||||
.response(responseBuilder().implementation(Notification.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> deleteNotification(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
var username = request.pathVariable("username");
|
||||
return notificationService.deleteByName(username, name)
|
||||
.flatMap(notification -> ServerResponse.ok().bodyValue(notification));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("api.notification.halo.run/v1alpha1");
|
||||
|
|
|
@ -122,7 +122,7 @@ metadata:
|
|||
rules:
|
||||
- apiGroups: [ "api.notification.halo.run" ]
|
||||
resources: [ "notifications" ]
|
||||
verbs: [ "get", "list" ]
|
||||
verbs: [ "get", "list", "delete" ]
|
||||
- apiGroups: [ "api.notification.halo.run" ]
|
||||
resources: [ "notifications/mark-as-read", "notifications/mark-specified-as-read" ]
|
||||
verbs: [ "update" ]
|
||||
|
|
|
@ -54,6 +54,63 @@ import { ReasonTypeNotifierMatrix } from "../models";
|
|||
export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator =
|
||||
function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* Delete the specified notification.
|
||||
* @param {string} username Username
|
||||
* @param {string} name Notification name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteSpecifiedNotification: async (
|
||||
username: string,
|
||||
name: string,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'username' is not null or undefined
|
||||
assertParamExists("deleteSpecifiedNotification", "username", username);
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("deleteSpecifiedNotification", "name", name);
|
||||
const localVarPath =
|
||||
`/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/{name}`
|
||||
.replace(`{${"username"}}`, encodeURIComponent(String(username)))
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "DELETE",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* List notification preferences for the authenticated user.
|
||||
* @param {string} username Username
|
||||
|
@ -422,6 +479,33 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFp = function (
|
|||
configuration
|
||||
);
|
||||
return {
|
||||
/**
|
||||
* Delete the specified notification.
|
||||
* @param {string} username Username
|
||||
* @param {string} name Notification name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteSpecifiedNotification(
|
||||
username: string,
|
||||
name: string,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Notification>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.deleteSpecifiedNotification(
|
||||
username,
|
||||
name,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* List notification preferences for the authenticated user.
|
||||
* @param {string} username Username
|
||||
|
@ -599,6 +683,24 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFactory = function (
|
|||
const localVarFp =
|
||||
ApiNotificationHaloRunV1alpha1NotificationApiFp(configuration);
|
||||
return {
|
||||
/**
|
||||
* Delete the specified notification.
|
||||
* @param {ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteSpecifiedNotification(
|
||||
requestParameters: ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Notification> {
|
||||
return localVarFp
|
||||
.deleteSpecifiedNotification(
|
||||
requestParameters.username,
|
||||
requestParameters.name,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List notification preferences for the authenticated user.
|
||||
* @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters.
|
||||
|
@ -695,6 +797,27 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFactory = function (
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for deleteSpecifiedNotification operation in ApiNotificationHaloRunV1alpha1NotificationApi.
|
||||
* @export
|
||||
* @interface ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest
|
||||
*/
|
||||
export interface ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest {
|
||||
/**
|
||||
* Username
|
||||
* @type {string}
|
||||
* @memberof ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotification
|
||||
*/
|
||||
readonly username: string;
|
||||
|
||||
/**
|
||||
* Notification name
|
||||
* @type {string}
|
||||
* @memberof ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotification
|
||||
*/
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listUserNotificationPreferences operation in ApiNotificationHaloRunV1alpha1NotificationApi.
|
||||
* @export
|
||||
|
@ -849,6 +972,26 @@ export interface ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificati
|
|||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ApiNotificationHaloRunV1alpha1NotificationApi extends BaseAPI {
|
||||
/**
|
||||
* Delete the specified notification.
|
||||
* @param {ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiNotificationHaloRunV1alpha1NotificationApi
|
||||
*/
|
||||
public deleteSpecifiedNotification(
|
||||
requestParameters: ApiNotificationHaloRunV1alpha1NotificationApiDeleteSpecifiedNotificationRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return ApiNotificationHaloRunV1alpha1NotificationApiFp(this.configuration)
|
||||
.deleteSpecifiedNotification(
|
||||
requestParameters.username,
|
||||
requestParameters.name,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List notification preferences for the authenticated user.
|
||||
* @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters.
|
||||
|
|
|
@ -37,6 +37,13 @@ const {
|
|||
return data;
|
||||
},
|
||||
cacheTime: 0,
|
||||
refetchInterval(data) {
|
||||
const hasDeletingNotifications = data?.items.some(
|
||||
(item) => item.metadata.deletionTimestamp !== undefined
|
||||
);
|
||||
|
||||
return hasDeletingNotifications ? 1000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedNotificationName = useRouteQuery<string | undefined>("name");
|
||||
|
|
|
@ -4,6 +4,7 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { relativeTimeTo } from "@/utils/date";
|
||||
import type { Notification } from "@halo-dev/api-client";
|
||||
import { useMutation, useQueryClient } from "@tanstack/vue-query";
|
||||
import { Dialog, Toast, VStatusDot } from "@halo-dev/components";
|
||||
import { watch } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
|
@ -40,6 +41,23 @@ const { mutate: handleMarkAsRead } = useMutation({
|
|||
},
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
Dialog.warning({
|
||||
title: "删除消息",
|
||||
description: "确定要删除该消息吗?",
|
||||
async onConfirm() {
|
||||
await apiClient.notification.deleteSpecifiedNotification({
|
||||
name: props.notification.metadata.name,
|
||||
username: currentUser?.metadata.name as string,
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["user-notifications"] });
|
||||
|
||||
Toast.success("删除成功");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.isSelected,
|
||||
(value) => {
|
||||
|
@ -61,11 +79,19 @@ watch(
|
|||
v-if="isSelected"
|
||||
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
||||
></div>
|
||||
<div
|
||||
class="truncate text-sm"
|
||||
:class="{ 'font-semibold': notification.spec?.unread && !isRead }"
|
||||
>
|
||||
{{ notification.spec?.title }}
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="truncate text-sm"
|
||||
:class="{ 'font-semibold': notification.spec?.unread && !isRead }"
|
||||
>
|
||||
{{ notification.spec?.title }}
|
||||
</div>
|
||||
<VStatusDot
|
||||
v-if="notification.metadata.deletionTimestamp"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.spec?.rawContent"
|
||||
|
@ -77,7 +103,7 @@ watch(
|
|||
<div class="text-xs text-gray-600">
|
||||
{{ relativeTimeTo(notification.metadata.creationTimestamp) }}
|
||||
</div>
|
||||
<div class="hidden group-hover:block">
|
||||
<div class="hidden space-x-2 group-hover:block">
|
||||
<span
|
||||
v-if="notification.spec?.unread && !isRead"
|
||||
class="text-sm text-gray-600 hover:text-gray-900"
|
||||
|
@ -85,6 +111,12 @@ watch(
|
|||
>
|
||||
{{ $t("core.notification.operations.mark_as_read.button") }}
|
||||
</span>
|
||||
<span
|
||||
class="text-sm text-red-600 hover:text-red-700"
|
||||
@click.stop="handleDelete"
|
||||
>
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,9 @@ api: |
|
|||
{{default "http://halo:8090" (env "SERVER")}}/apis
|
||||
param:
|
||||
postName: "{{randAlpha 6}}"
|
||||
userName: "{{randAlpha 6}}"
|
||||
notificationName: "{{randAlpha 6}}"
|
||||
auth: "Basic YWRtaW46MTIzNDU2"
|
||||
items:
|
||||
- name: init
|
||||
request:
|
||||
|
@ -84,3 +87,48 @@ items:
|
|||
method: DELETE
|
||||
header:
|
||||
Authorization: "Basic YWRtaW46MTIzNDU2"
|
||||
|
||||
# Notifications
|
||||
- name: createNotification
|
||||
request:
|
||||
api: /notification.halo.run/v1alpha1/notifications
|
||||
method: POST
|
||||
body: |
|
||||
{
|
||||
"spec": {
|
||||
"recipient": "admin",
|
||||
"reason": "fake-reason",
|
||||
"title": "test 评论了你的页面《关于我》",
|
||||
"rawContent": "Fake raw content",
|
||||
"htmlContent": "<p>Fake html content</p>",
|
||||
"unread": true
|
||||
},
|
||||
"apiVersion": "notification.halo.run/v1alpha1",
|
||||
"kind": "Notification",
|
||||
"metadata": {
|
||||
"name": "{{.param.notificationName}}"
|
||||
}
|
||||
}
|
||||
header:
|
||||
Content-Type: application/json
|
||||
Authorization: "{{.param.auth}}"
|
||||
expect:
|
||||
statusCode: 201
|
||||
- name: getNotificationByName
|
||||
request:
|
||||
api: /notification.halo.run/v1alpha1/notifications/{{.param.notificationName}}
|
||||
method: GET
|
||||
header:
|
||||
Authorization: "{{.param.auth}}"
|
||||
expect:
|
||||
statusCode: 200
|
||||
verify:
|
||||
- data.spec.reason == "fake-reason"
|
||||
- data.spec.title == "test 评论了你的页面《关于我》"
|
||||
- name: deleteUserNotification
|
||||
request:
|
||||
api: |
|
||||
/api.notification.halo.run/v1alpha1/userspaces/admin/notifications/{{.param.notificationName}}
|
||||
method: DELETE
|
||||
header:
|
||||
Authorization: "{{.param.auth}}"
|
||||
|
|
Loading…
Reference in New Issue