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
guqing 2023-11-27 21:58:09 +08:00 committed by GitHub
parent 6d6b1611d8
commit 91affebdd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 278 additions and 7 deletions

View File

@ -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");

View File

@ -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);
}

View File

@ -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");

View File

@ -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" ]

View File

@ -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.

View File

@ -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");

View File

@ -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>

View File

@ -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}}"