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.core.extension.notification.Notification;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.infra.exception.AccessDeniedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A default implementation of {@link UserNotificationService}.
|
* A default implementation of {@link UserNotificationService}.
|
||||||
|
@ -49,6 +50,19 @@ public class DefaultNotificationService implements UserNotificationService {
|
||||||
.map(notification -> notification.getMetadata().getName());
|
.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) {
|
static boolean isRecipient(Notification notification, String username) {
|
||||||
Assert.notNull(notification, "Notification must not be null");
|
Assert.notNull(notification, "Notification must not be null");
|
||||||
Assert.notNull(username, "Username 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
|
* @return the names of read notification that has been marked as read
|
||||||
*/
|
*/
|
||||||
Flux<String> markSpecifiedAsRead(String username, List<String> names);
|
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))
|
.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();
|
.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
|
@Override
|
||||||
public GroupVersion groupVersion() {
|
public GroupVersion groupVersion() {
|
||||||
return GroupVersion.parseAPIVersion("api.notification.halo.run/v1alpha1");
|
return GroupVersion.parseAPIVersion("api.notification.halo.run/v1alpha1");
|
||||||
|
|
|
@ -122,7 +122,7 @@ metadata:
|
||||||
rules:
|
rules:
|
||||||
- apiGroups: [ "api.notification.halo.run" ]
|
- apiGroups: [ "api.notification.halo.run" ]
|
||||||
resources: [ "notifications" ]
|
resources: [ "notifications" ]
|
||||||
verbs: [ "get", "list" ]
|
verbs: [ "get", "list", "delete" ]
|
||||||
- apiGroups: [ "api.notification.halo.run" ]
|
- apiGroups: [ "api.notification.halo.run" ]
|
||||||
resources: [ "notifications/mark-as-read", "notifications/mark-specified-as-read" ]
|
resources: [ "notifications/mark-as-read", "notifications/mark-specified-as-read" ]
|
||||||
verbs: [ "update" ]
|
verbs: [ "update" ]
|
||||||
|
|
|
@ -54,6 +54,63 @@ import { ReasonTypeNotifierMatrix } from "../models";
|
||||||
export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator =
|
export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator =
|
||||||
function (configuration?: Configuration) {
|
function (configuration?: Configuration) {
|
||||||
return {
|
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.
|
* List notification preferences for the authenticated user.
|
||||||
* @param {string} username Username
|
* @param {string} username Username
|
||||||
|
@ -422,6 +479,33 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFp = function (
|
||||||
configuration
|
configuration
|
||||||
);
|
);
|
||||||
return {
|
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.
|
* List notification preferences for the authenticated user.
|
||||||
* @param {string} username Username
|
* @param {string} username Username
|
||||||
|
@ -599,6 +683,24 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFactory = function (
|
||||||
const localVarFp =
|
const localVarFp =
|
||||||
ApiNotificationHaloRunV1alpha1NotificationApiFp(configuration);
|
ApiNotificationHaloRunV1alpha1NotificationApiFp(configuration);
|
||||||
return {
|
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.
|
* List notification preferences for the authenticated user.
|
||||||
* @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters.
|
* @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.
|
* Request parameters for listUserNotificationPreferences operation in ApiNotificationHaloRunV1alpha1NotificationApi.
|
||||||
* @export
|
* @export
|
||||||
|
@ -849,6 +972,26 @@ export interface ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificati
|
||||||
* @extends {BaseAPI}
|
* @extends {BaseAPI}
|
||||||
*/
|
*/
|
||||||
export class ApiNotificationHaloRunV1alpha1NotificationApi 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.
|
* List notification preferences for the authenticated user.
|
||||||
* @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters.
|
* @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters.
|
||||||
|
|
|
@ -37,6 +37,13 @@ const {
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
cacheTime: 0,
|
cacheTime: 0,
|
||||||
|
refetchInterval(data) {
|
||||||
|
const hasDeletingNotifications = data?.items.some(
|
||||||
|
(item) => item.metadata.deletionTimestamp !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasDeletingNotifications ? 1000 : false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedNotificationName = useRouteQuery<string | undefined>("name");
|
const selectedNotificationName = useRouteQuery<string | undefined>("name");
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { apiClient } from "@/utils/api-client";
|
||||||
import { relativeTimeTo } from "@/utils/date";
|
import { relativeTimeTo } from "@/utils/date";
|
||||||
import type { Notification } from "@halo-dev/api-client";
|
import type { Notification } from "@halo-dev/api-client";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/vue-query";
|
import { useMutation, useQueryClient } from "@tanstack/vue-query";
|
||||||
|
import { Dialog, Toast, VStatusDot } from "@halo-dev/components";
|
||||||
import { watch } from "vue";
|
import { watch } from "vue";
|
||||||
import { ref } 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(
|
watch(
|
||||||
() => props.isSelected,
|
() => props.isSelected,
|
||||||
(value) => {
|
(value) => {
|
||||||
|
@ -61,11 +79,19 @@ watch(
|
||||||
v-if="isSelected"
|
v-if="isSelected"
|
||||||
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div class="flex items-center justify-between">
|
||||||
class="truncate text-sm"
|
<div
|
||||||
:class="{ 'font-semibold': notification.spec?.unread && !isRead }"
|
class="truncate text-sm"
|
||||||
>
|
:class="{ 'font-semibold': notification.spec?.unread && !isRead }"
|
||||||
{{ notification.spec?.title }}
|
>
|
||||||
|
{{ notification.spec?.title }}
|
||||||
|
</div>
|
||||||
|
<VStatusDot
|
||||||
|
v-if="notification.metadata.deletionTimestamp"
|
||||||
|
v-tooltip="$t('core.common.status.deleting')"
|
||||||
|
state="warning"
|
||||||
|
animate
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="notification.spec?.rawContent"
|
v-if="notification.spec?.rawContent"
|
||||||
|
@ -77,7 +103,7 @@ watch(
|
||||||
<div class="text-xs text-gray-600">
|
<div class="text-xs text-gray-600">
|
||||||
{{ relativeTimeTo(notification.metadata.creationTimestamp) }}
|
{{ relativeTimeTo(notification.metadata.creationTimestamp) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden group-hover:block">
|
<div class="hidden space-x-2 group-hover:block">
|
||||||
<span
|
<span
|
||||||
v-if="notification.spec?.unread && !isRead"
|
v-if="notification.spec?.unread && !isRead"
|
||||||
class="text-sm text-gray-600 hover:text-gray-900"
|
class="text-sm text-gray-600 hover:text-gray-900"
|
||||||
|
@ -85,6 +111,12 @@ watch(
|
||||||
>
|
>
|
||||||
{{ $t("core.notification.operations.mark_as_read.button") }}
|
{{ $t("core.notification.operations.mark_as_read.button") }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class="text-sm text-red-600 hover:text-red-700"
|
||||||
|
@click.stop="handleDelete"
|
||||||
|
>
|
||||||
|
{{ $t("core.common.buttons.delete") }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,9 @@ api: |
|
||||||
{{default "http://halo:8090" (env "SERVER")}}/apis
|
{{default "http://halo:8090" (env "SERVER")}}/apis
|
||||||
param:
|
param:
|
||||||
postName: "{{randAlpha 6}}"
|
postName: "{{randAlpha 6}}"
|
||||||
|
userName: "{{randAlpha 6}}"
|
||||||
|
notificationName: "{{randAlpha 6}}"
|
||||||
|
auth: "Basic YWRtaW46MTIzNDU2"
|
||||||
items:
|
items:
|
||||||
- name: init
|
- name: init
|
||||||
request:
|
request:
|
||||||
|
@ -84,3 +87,48 @@ items:
|
||||||
method: DELETE
|
method: DELETE
|
||||||
header:
|
header:
|
||||||
Authorization: "Basic YWRtaW46MTIzNDU2"
|
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