diff --git a/application/src/main/java/run/halo/app/notification/UserNotificationPreference.java b/application/src/main/java/run/halo/app/notification/UserNotificationPreference.java index 4762931529..54e259ee83 100644 --- a/application/src/main/java/run/halo/app/notification/UserNotificationPreference.java +++ b/application/src/main/java/run/halo/app/notification/UserNotificationPreference.java @@ -6,6 +6,7 @@ import java.util.Set; import lombok.Data; import lombok.Getter; +import org.springframework.lang.NonNull; /** * Notification preference of user. @@ -28,6 +29,7 @@ public static class ReasonTypeNotifier extends HashMap * @return if key of reasonType not exists, return default notifier, otherwise return the * notifiers */ + @NonNull public Set getNotifiers(String reasonType) { var result = this.get(reasonType); return result == null ? Set.of(DEFAULT_NOTIFIER) diff --git a/application/src/main/java/run/halo/app/notification/UserNotificationPreferenceService.java b/application/src/main/java/run/halo/app/notification/UserNotificationPreferenceService.java index a83eb33aaf..69ede65f18 100644 --- a/application/src/main/java/run/halo/app/notification/UserNotificationPreferenceService.java +++ b/application/src/main/java/run/halo/app/notification/UserNotificationPreferenceService.java @@ -11,4 +11,7 @@ public interface UserNotificationPreferenceService { Mono getByUser(String username); + + Mono saveByUser(String username, + UserNotificationPreference userNotificationPreference); } diff --git a/application/src/main/java/run/halo/app/notification/UserNotificationPreferenceServiceImpl.java b/application/src/main/java/run/halo/app/notification/UserNotificationPreferenceServiceImpl.java index bcb50dcbc8..88c88bd4c5 100644 --- a/application/src/main/java/run/halo/app/notification/UserNotificationPreferenceServiceImpl.java +++ b/application/src/main/java/run/halo/app/notification/UserNotificationPreferenceServiceImpl.java @@ -1,10 +1,12 @@ package run.halo.app.notification; +import java.util.HashMap; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.utils.JsonUtils; @@ -39,6 +41,28 @@ public Mono getByUser(String username) { .defaultIfEmpty(new UserNotificationPreference()); } + @Override + public Mono saveByUser(String username, + UserNotificationPreference userNotificationPreference) { + var configName = buildUserPreferenceConfigMapName(username); + return client.fetch(ConfigMap.class, configName) + .switchIfEmpty(Mono.defer(() -> { + var configMap = new ConfigMap(); + configMap.setMetadata(new Metadata()); + configMap.getMetadata().setName(configName); + return client.create(configMap); + })) + .flatMap(config -> { + if (config.getData() == null) { + config.setData(new HashMap<>()); + } + config.getData().put(NOTIFICATION_PREFERENCE, + JsonUtils.objectToJson(userNotificationPreference)); + return client.update(config); + }) + .then(); + } + static String buildUserPreferenceConfigMapName(String username) { return "user-preferences-" + username; } diff --git a/application/src/main/java/run/halo/app/notification/endpoint/UserNotificationPreferencesEndpoint.java b/application/src/main/java/run/halo/app/notification/endpoint/UserNotificationPreferencesEndpoint.java new file mode 100644 index 0000000000..90948904d0 --- /dev/null +++ b/application/src/main/java/run/halo/app/notification/endpoint/UserNotificationPreferencesEndpoint.java @@ -0,0 +1,211 @@ +package run.halo.app.notification.endpoint; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; + +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; +import run.halo.app.core.extension.endpoint.CustomEndpoint; +import run.halo.app.core.extension.notification.NotifierDescriptor; +import run.halo.app.core.extension.notification.ReasonType; +import run.halo.app.extension.Comparators; +import run.halo.app.extension.GroupVersion; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.notification.UserNotificationPreference; +import run.halo.app.notification.UserNotificationPreferenceService; + +/** + * Endpoint for user notification preferences. + * + * @author guqing + * @since 2.10.0 + */ +@Component +@RequiredArgsConstructor +public class UserNotificationPreferencesEndpoint implements CustomEndpoint { + + private final ReactiveExtensionClient client; + private final UserNotificationPreferenceService userNotificationPreferenceService; + + @Override + public RouterFunction endpoint() { + return SpringdocRouteBuilder.route() + .nest(RequestPredicates.path("/userspaces/{username}"), userspaceScopedApis(), + builder -> { + }) + .build(); + } + + Supplier> userspaceScopedApis() { + var tag = "api.notification.halo.run/v1alpha1/Notification"; + return () -> SpringdocRouteBuilder.route() + .GET("/notification-preferences", this::listNotificationPreferences, + builder -> builder.operationId("ListUserNotificationPreferences") + .description("List notification preferences for the authenticated user.") + .tag(tag) + .parameter(parameterBuilder() + .in(ParameterIn.PATH) + .name("username") + .description("Username") + .required(true) + ) + .response(responseBuilder() + .implementation(ReasonTypeNotifierMatrix.class) + ) + ) + .POST("/notification-preferences", this::saveNotificationPreferences, + builder -> builder.operationId("SaveUserNotificationPreferences") + .description("Save notification preferences for the authenticated user.") + .tag(tag) + .parameter(parameterBuilder() + .in(ParameterIn.PATH) + .name("username") + .description("Username") + .required(true) + ) + .requestBody(requestBodyBuilder() + .implementation(ReasonTypeNotifierCollectionRequest.class) + ) + .response(responseBuilder().implementation(ReasonTypeNotifierMatrix.class)) + ) + .build(); + } + + private Mono saveNotificationPreferences(ServerRequest request) { + var username = request.pathVariable("username"); + return request.bodyToMono(ReasonTypeNotifierCollectionRequest.class) + .flatMap(requestBody -> { + var reasonTypNotifiers = requestBody.reasonTypeNotifiers(); + return userNotificationPreferenceService.getByUser(username) + .flatMap(preference -> { + var reasonTypeNotifierMap = preference.getReasonTypeNotifier(); + reasonTypeNotifierMap.clear(); + reasonTypNotifiers.forEach(reasonTypeNotifierRequest -> { + var reasonType = reasonTypeNotifierRequest.getReasonType(); + var notifiers = reasonTypeNotifierRequest.getNotifiers(); + var notifierSetting = new UserNotificationPreference.NotifierSetting(); + notifierSetting.setNotifiers( + notifiers == null ? Set.of() : Set.copyOf(notifiers)); + reasonTypeNotifierMap.put(reasonType, notifierSetting); + }); + return userNotificationPreferenceService.saveByUser(username, preference); + }); + }) + .then(Mono.defer(() -> listReasonTypeNotifierMatrix(username) + .flatMap(result -> ServerResponse.ok().bodyValue(result))) + ); + } + + private Mono listNotificationPreferences(ServerRequest request) { + var username = request.pathVariable("username"); + return listReasonTypeNotifierMatrix(username) + .flatMap(matrix -> ServerResponse.ok().bodyValue(matrix)); + } + + @NonNull + private static Map toNameIndexMap(List collection, + Function nameGetter) { + Map indexMap = new HashMap<>(); + for (int i = 0; i < collection.size(); i++) { + var item = collection.get(i); + indexMap.put(nameGetter.apply(item), i); + } + return indexMap; + } + + Mono listReasonTypeNotifierMatrix(String username) { + return client.list(ReasonType.class, null, Comparators.defaultComparator()) + .map(reasonType -> new ReasonTypeInfo(reasonType.getMetadata().getName(), + reasonType.getSpec().getDisplayName(), + reasonType.getSpec().getDescription()) + ) + .collectList() + .flatMap(reasonTypes -> client.list(NotifierDescriptor.class, null, + Comparators.defaultComparator()) + .map(notifier -> new NotifierInfo(notifier.getMetadata().getName(), + notifier.getSpec().getDisplayName(), + notifier.getSpec().getDescription()) + ) + .collectList() + .map(notifiers -> { + var matrix = new ReasonTypeNotifierMatrix() + .setReasonTypes(reasonTypes) + .setNotifiers(notifiers) + .setStateMatrix(new boolean[reasonTypes.size()][notifiers.size()]); + return Tuples.of(reasonTypes, matrix); + }) + ) + .flatMap(tuple2 -> { + var reasonTypes = tuple2.getT1(); + var matrix = tuple2.getT2(); + + var reasonTypeIndexMap = toNameIndexMap(reasonTypes, ReasonTypeInfo::name); + var notifierIndexMap = toNameIndexMap(matrix.getNotifiers(), NotifierInfo::name); + var stateMatrix = matrix.getStateMatrix(); + + return userNotificationPreferenceService.getByUser(username) + .doOnNext(preference -> { + var reasonTypeNotifierMap = preference.getReasonTypeNotifier(); + for (ReasonTypeInfo reasonType : reasonTypes) { + var reasonTypeIndex = reasonTypeIndexMap.get(reasonType.name()); + var notifierNames = + reasonTypeNotifierMap.getNotifiers(reasonType.name()); + for (String notifierName : notifierNames) { + var notifierIndex = notifierIndexMap.get(notifierName); + stateMatrix[reasonTypeIndex][notifierIndex] = true; + } + } + }) + .thenReturn(matrix); + }); + } + + @Data + @Accessors(chain = true) + static class ReasonTypeNotifierMatrix { + private List reasonTypes; + private List notifiers; + private boolean[][] stateMatrix; + } + + record ReasonTypeInfo(String name, String displayName, String description) { + } + + record NotifierInfo(String name, String displayName, String description) { + } + + record ReasonTypeNotifierCollectionRequest( + @Schema(requiredMode = REQUIRED) List reasonTypeNotifiers) { + } + + @Data + static class ReasonTypeNotifierRequest { + private String reasonType; + private List notifiers; + } + + @Override + public GroupVersion groupVersion() { + return GroupVersion.parseAPIVersion("api.notification.halo.run/v1alpha1"); + } +} diff --git a/application/src/main/resources/extensions/role-template-authenticated.yaml b/application/src/main/resources/extensions/role-template-authenticated.yaml index 0424b7544c..dff7d74dcc 100644 --- a/application/src/main/resources/extensions/role-template-authenticated.yaml +++ b/application/src/main/resources/extensions/role-template-authenticated.yaml @@ -129,3 +129,6 @@ rules: - apiGroups: [ "api.notification.halo.run" ] resources: [ "notifiers/receiver-config" ] verbs: [ "get", "update" ] + - apiGroups: [ "api.notification.halo.run" ] + resources: [ "notification-preferences" ] + verbs: [ "create", "list" ] diff --git a/application/src/test/java/run/halo/app/notification/endpoint/UserNotificationPreferencesEndpointTest.java b/application/src/test/java/run/halo/app/notification/endpoint/UserNotificationPreferencesEndpointTest.java new file mode 100644 index 0000000000..f5aa3352c5 --- /dev/null +++ b/application/src/test/java/run/halo/app/notification/endpoint/UserNotificationPreferencesEndpointTest.java @@ -0,0 +1,71 @@ +package run.halo.app.notification.endpoint; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.notification.NotifierDescriptor; +import run.halo.app.core.extension.notification.ReasonType; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.notification.UserNotificationPreferenceService; + +/** + * Tests for {@link UserNotificationPreferencesEndpoint}. + * + * @author guqing + * @since 2.10.0 + */ +@ExtendWith(MockitoExtension.class) +class UserNotificationPreferencesEndpointTest { + + @Mock + private ReactiveExtensionClient client; + + @Mock + private UserNotificationPreferenceService userNotificationPreferenceService; + + @InjectMocks + private UserNotificationPreferencesEndpoint userNotificationPreferencesEndpoint; + + private WebTestClient webTestClient; + + @BeforeEach + void setUp() { + webTestClient = WebTestClient + .bindToRouterFunction(userNotificationPreferencesEndpoint.endpoint()) + .build(); + } + + @Test + void listNotificationPreferences() { + when(client.list(eq(ReasonType.class), eq(null), any())).thenReturn(Flux.empty()); + when(client.list(eq(NotifierDescriptor.class), eq(null), any())).thenReturn(Flux.empty()); + when(userNotificationPreferenceService.getByUser(any())).thenReturn(Mono.empty()); + webTestClient.post() + .uri("/userspaces/{username}/notification-preferences", "guqing") + .exchange() + .expectStatus() + .isOk(); + } + + @Test + void saveNotificationPreferences() { + when(client.list(eq(ReasonType.class), eq(null), any())).thenReturn(Flux.empty()); + when(client.list(eq(NotifierDescriptor.class), eq(null), any())).thenReturn(Flux.empty()); + when(userNotificationPreferenceService.getByUser(any())).thenReturn(Mono.empty()); + webTestClient.post() + .uri("/userspaces/{username}/notification-preferences", "guqing") + .exchange() + .expectStatus() + .isOk(); + } +} \ No newline at end of file diff --git a/console/packages/api-client/src/.openapi-generator/FILES b/console/packages/api-client/src/.openapi-generator/FILES index edf8d55b42..2f683a17ee 100644 --- a/console/packages/api-client/src/.openapi-generator/FILES +++ b/console/packages/api-client/src/.openapi-generator/FILES @@ -179,6 +179,7 @@ models/notification.ts models/notifier-descriptor-list.ts models/notifier-descriptor-spec.ts models/notifier-descriptor.ts +models/notifier-info.ts models/notifier-setting-ref.ts models/owner-info.ts models/pat-spec.ts @@ -211,7 +212,11 @@ models/reason-selector.ts models/reason-spec-attributes.ts models/reason-spec.ts models/reason-subject.ts +models/reason-type-info.ts models/reason-type-list.ts +models/reason-type-notifier-collection-request.ts +models/reason-type-notifier-matrix.ts +models/reason-type-notifier-request.ts models/reason-type-spec.ts models/reason-type.ts models/reason.ts diff --git a/console/packages/api-client/src/api/api-notification-halo-run-v1alpha1-notification-api.ts b/console/packages/api-client/src/api/api-notification-halo-run-v1alpha1-notification-api.ts index e6be0e109c..df4dd56664 100644 --- a/console/packages/api-client/src/api/api-notification-halo-run-v1alpha1-notification-api.ts +++ b/console/packages/api-client/src/api/api-notification-halo-run-v1alpha1-notification-api.ts @@ -43,6 +43,10 @@ import { MarkSpecifiedRequest } from "../models"; import { Notification } from "../models"; // @ts-ignore import { NotificationList } from "../models"; +// @ts-ignore +import { ReasonTypeNotifierCollectionRequest } from "../models"; +// @ts-ignore +import { ReasonTypeNotifierMatrix } from "../models"; /** * ApiNotificationHaloRunV1alpha1NotificationApi - axios parameter creator * @export @@ -50,6 +54,64 @@ import { NotificationList } from "../models"; export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * List notification preferences for the authenticated user. + * @param {string} username Username + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listUserNotificationPreferences: async ( + username: string, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'username' is not null or undefined + assertParamExists( + "listUserNotificationPreferences", + "username", + username + ); + const localVarPath = + `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notification-preferences`.replace( + `{${"username"}}`, + encodeURIComponent(String(username)) + ); + // 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: "GET", + ...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 notifications for the authenticated user. * @param {string} username Username @@ -273,6 +335,73 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator = configuration ); + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Save notification preferences for the authenticated user. + * @param {string} username Username + * @param {ReasonTypeNotifierCollectionRequest} [reasonTypeNotifierCollectionRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + saveUserNotificationPreferences: async ( + username: string, + reasonTypeNotifierCollectionRequest?: ReasonTypeNotifierCollectionRequest, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'username' is not null or undefined + assertParamExists( + "saveUserNotificationPreferences", + "username", + username + ); + const localVarPath = + `/apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notification-preferences`.replace( + `{${"username"}}`, + encodeURIComponent(String(username)) + ); + // 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: "POST", + ...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); + + localVarHeaderParameter["Content-Type"] = "application/json"; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + localVarRequestOptions.data = serializeDataIfNeeded( + reasonTypeNotifierCollectionRequest, + localVarRequestOptions, + configuration + ); + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -293,6 +422,33 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFp = function ( configuration ); return { + /** + * List notification preferences for the authenticated user. + * @param {string} username Username + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async listUserNotificationPreferences( + username: string, + options?: AxiosRequestConfig + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string + ) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.listUserNotificationPreferences( + username, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, /** * List notifications for the authenticated user. * @param {string} username Username @@ -398,6 +554,36 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFp = function ( configuration ); }, + /** + * Save notification preferences for the authenticated user. + * @param {string} username Username + * @param {ReasonTypeNotifierCollectionRequest} [reasonTypeNotifierCollectionRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async saveUserNotificationPreferences( + username: string, + reasonTypeNotifierCollectionRequest?: ReasonTypeNotifierCollectionRequest, + options?: AxiosRequestConfig + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string + ) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.saveUserNotificationPreferences( + username, + reasonTypeNotifierCollectionRequest, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, }; }; @@ -413,6 +599,20 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFactory = function ( const localVarFp = ApiNotificationHaloRunV1alpha1NotificationApiFp(configuration); return { + /** + * List notification preferences for the authenticated user. + * @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listUserNotificationPreferences( + requestParameters: ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest, + options?: AxiosRequestConfig + ): AxiosPromise { + return localVarFp + .listUserNotificationPreferences(requestParameters.username, options) + .then((request) => request(axios, basePath)); + }, /** * List notifications for the authenticated user. * @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationsRequest} requestParameters Request parameters. @@ -474,9 +674,41 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFactory = function ( ) .then((request) => request(axios, basePath)); }, + /** + * Save notification preferences for the authenticated user. + * @param {ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificationPreferencesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + saveUserNotificationPreferences( + requestParameters: ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificationPreferencesRequest, + options?: AxiosRequestConfig + ): AxiosPromise { + return localVarFp + .saveUserNotificationPreferences( + requestParameters.username, + requestParameters.reasonTypeNotifierCollectionRequest, + options + ) + .then((request) => request(axios, basePath)); + }, }; }; +/** + * Request parameters for listUserNotificationPreferences operation in ApiNotificationHaloRunV1alpha1NotificationApi. + * @export + * @interface ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest + */ +export interface ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest { + /** + * Username + * @type {string} + * @memberof ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferences + */ + readonly username: string; +} + /** * Request parameters for listUserNotifications operation in ApiNotificationHaloRunV1alpha1NotificationApi. * @export @@ -589,6 +821,27 @@ export interface ApiNotificationHaloRunV1alpha1NotificationApiMarkNotificationsA readonly markSpecifiedRequest: MarkSpecifiedRequest; } +/** + * Request parameters for saveUserNotificationPreferences operation in ApiNotificationHaloRunV1alpha1NotificationApi. + * @export + * @interface ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificationPreferencesRequest + */ +export interface ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificationPreferencesRequest { + /** + * Username + * @type {string} + * @memberof ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificationPreferences + */ + readonly username: string; + + /** + * + * @type {ReasonTypeNotifierCollectionRequest} + * @memberof ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificationPreferences + */ + readonly reasonTypeNotifierCollectionRequest?: ReasonTypeNotifierCollectionRequest; +} + /** * ApiNotificationHaloRunV1alpha1NotificationApi - object-oriented interface * @export @@ -596,6 +849,22 @@ export interface ApiNotificationHaloRunV1alpha1NotificationApiMarkNotificationsA * @extends {BaseAPI} */ export class ApiNotificationHaloRunV1alpha1NotificationApi extends BaseAPI { + /** + * List notification preferences for the authenticated user. + * @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiNotificationHaloRunV1alpha1NotificationApi + */ + public listUserNotificationPreferences( + requestParameters: ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationPreferencesRequest, + options?: AxiosRequestConfig + ) { + return ApiNotificationHaloRunV1alpha1NotificationApiFp(this.configuration) + .listUserNotificationPreferences(requestParameters.username, options) + .then((request) => request(this.axios, this.basePath)); + } + /** * List notifications for the authenticated user. * @param {ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificationsRequest} requestParameters Request parameters. @@ -662,4 +931,24 @@ export class ApiNotificationHaloRunV1alpha1NotificationApi extends BaseAPI { ) .then((request) => request(this.axios, this.basePath)); } + + /** + * Save notification preferences for the authenticated user. + * @param {ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificationPreferencesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiNotificationHaloRunV1alpha1NotificationApi + */ + public saveUserNotificationPreferences( + requestParameters: ApiNotificationHaloRunV1alpha1NotificationApiSaveUserNotificationPreferencesRequest, + options?: AxiosRequestConfig + ) { + return ApiNotificationHaloRunV1alpha1NotificationApiFp(this.configuration) + .saveUserNotificationPreferences( + requestParameters.username, + requestParameters.reasonTypeNotifierCollectionRequest, + options + ) + .then((request) => request(this.axios, this.basePath)); + } } diff --git a/console/packages/api-client/src/models/index.ts b/console/packages/api-client/src/models/index.ts index a1f22074de..46e5a3e834 100644 --- a/console/packages/api-client/src/models/index.ts +++ b/console/packages/api-client/src/models/index.ts @@ -102,6 +102,7 @@ export * from "./notification-template-spec"; export * from "./notifier-descriptor"; export * from "./notifier-descriptor-list"; export * from "./notifier-descriptor-spec"; +export * from "./notifier-info"; export * from "./notifier-setting-ref"; export * from "./owner-info"; export * from "./pat-spec"; @@ -136,7 +137,11 @@ export * from "./reason-spec"; export * from "./reason-spec-attributes"; export * from "./reason-subject"; export * from "./reason-type"; +export * from "./reason-type-info"; export * from "./reason-type-list"; +export * from "./reason-type-notifier-collection-request"; +export * from "./reason-type-notifier-matrix"; +export * from "./reason-type-notifier-request"; export * from "./reason-type-spec"; export * from "./ref"; export * from "./reply"; diff --git a/console/packages/api-client/src/models/notifier-info.ts b/console/packages/api-client/src/models/notifier-info.ts new file mode 100644 index 0000000000..f10b6f9f07 --- /dev/null +++ b/console/packages/api-client/src/models/notifier-info.ts @@ -0,0 +1,39 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo Next API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface NotifierInfo + */ +export interface NotifierInfo { + /** + * + * @type {string} + * @memberof NotifierInfo + */ + description?: string; + /** + * + * @type {string} + * @memberof NotifierInfo + */ + displayName?: string; + /** + * + * @type {string} + * @memberof NotifierInfo + */ + name?: string; +} diff --git a/console/packages/api-client/src/models/reason-type-info.ts b/console/packages/api-client/src/models/reason-type-info.ts new file mode 100644 index 0000000000..1008af2c5e --- /dev/null +++ b/console/packages/api-client/src/models/reason-type-info.ts @@ -0,0 +1,39 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo Next API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface ReasonTypeInfo + */ +export interface ReasonTypeInfo { + /** + * + * @type {string} + * @memberof ReasonTypeInfo + */ + description?: string; + /** + * + * @type {string} + * @memberof ReasonTypeInfo + */ + displayName?: string; + /** + * + * @type {string} + * @memberof ReasonTypeInfo + */ + name?: string; +} diff --git a/console/packages/api-client/src/models/reason-type-notifier-collection-request.ts b/console/packages/api-client/src/models/reason-type-notifier-collection-request.ts new file mode 100644 index 0000000000..908c3b1cd7 --- /dev/null +++ b/console/packages/api-client/src/models/reason-type-notifier-collection-request.ts @@ -0,0 +1,31 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo Next API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +// May contain unused imports in some cases +// @ts-ignore +import { ReasonTypeNotifierRequest } from "./reason-type-notifier-request"; + +/** + * + * @export + * @interface ReasonTypeNotifierCollectionRequest + */ +export interface ReasonTypeNotifierCollectionRequest { + /** + * + * @type {Array} + * @memberof ReasonTypeNotifierCollectionRequest + */ + reasonTypeNotifiers: Array; +} diff --git a/console/packages/api-client/src/models/reason-type-notifier-matrix-item.ts b/console/packages/api-client/src/models/reason-type-notifier-matrix-item.ts new file mode 100644 index 0000000000..9cc8784424 --- /dev/null +++ b/console/packages/api-client/src/models/reason-type-notifier-matrix-item.ts @@ -0,0 +1,46 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo Next API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +// May contain unused imports in some cases +// @ts-ignore +import { NotifierInfo } from "./notifier-info"; +// May contain unused imports in some cases +// @ts-ignore +import { ReasonTypeInfo } from "./reason-type-info"; + +/** + * + * @export + * @interface ReasonTypeNotifierMatrixItem + */ +export interface ReasonTypeNotifierMatrixItem { + /** + * + * @type {boolean} + * @memberof ReasonTypeNotifierMatrixItem + */ + enabled?: boolean; + /** + * + * @type {NotifierInfo} + * @memberof ReasonTypeNotifierMatrixItem + */ + notifier?: NotifierInfo; + /** + * + * @type {ReasonTypeInfo} + * @memberof ReasonTypeNotifierMatrixItem + */ + reasonType?: ReasonTypeInfo; +} diff --git a/console/packages/api-client/src/models/reason-type-notifier-matrix.ts b/console/packages/api-client/src/models/reason-type-notifier-matrix.ts new file mode 100644 index 0000000000..3ce315c767 --- /dev/null +++ b/console/packages/api-client/src/models/reason-type-notifier-matrix.ts @@ -0,0 +1,46 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo Next API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +// May contain unused imports in some cases +// @ts-ignore +import { NotifierInfo } from "./notifier-info"; +// May contain unused imports in some cases +// @ts-ignore +import { ReasonTypeInfo } from "./reason-type-info"; + +/** + * + * @export + * @interface ReasonTypeNotifierMatrix + */ +export interface ReasonTypeNotifierMatrix { + /** + * + * @type {Array} + * @memberof ReasonTypeNotifierMatrix + */ + notifiers?: Array; + /** + * + * @type {Array} + * @memberof ReasonTypeNotifierMatrix + */ + reasonTypes?: Array; + /** + * + * @type {Array>} + * @memberof ReasonTypeNotifierMatrix + */ + stateMatrix?: Array>; +} diff --git a/console/packages/api-client/src/models/reason-type-notifier-request.ts b/console/packages/api-client/src/models/reason-type-notifier-request.ts new file mode 100644 index 0000000000..5d71bf99ce --- /dev/null +++ b/console/packages/api-client/src/models/reason-type-notifier-request.ts @@ -0,0 +1,33 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo Next API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface ReasonTypeNotifierRequest + */ +export interface ReasonTypeNotifierRequest { + /** + * + * @type {Array} + * @memberof ReasonTypeNotifierRequest + */ + notifiers?: Array; + /** + * + * @type {string} + * @memberof ReasonTypeNotifierRequest + */ + reasonType?: string; +} diff --git a/console/src/locales/en.yaml b/console/src/locales/en.yaml index 8d655d985a..cbcb5caa6b 100644 --- a/console/src/locales/en.yaml +++ b/console/src/locales/en.yaml @@ -854,6 +854,7 @@ core: title: User detail tabs: detail: Detail + notification-preferences: Notification Preferences pat: Personal Access Tokens actions: update_profile: @@ -929,6 +930,9 @@ core: help: Leave empty for no expiration description: label: Description + notification-preferences: + fields: + type: Type role: title: Roles common: diff --git a/console/src/locales/zh-CN.yaml b/console/src/locales/zh-CN.yaml index 2a3f9895d4..06ba6b5634 100644 --- a/console/src/locales/zh-CN.yaml +++ b/console/src/locales/zh-CN.yaml @@ -854,6 +854,7 @@ core: title: 用户详情 tabs: detail: 详情 + notification-preferences: 通知配置 pat: 个人令牌 actions: update_profile: @@ -929,6 +930,9 @@ core: help: 不设置代表永不过期 description: label: 描述 + notification-preferences: + fields: + type: 通知类型 role: title: 角色 common: diff --git a/console/src/locales/zh-TW.yaml b/console/src/locales/zh-TW.yaml index 0029781fab..fa3e3e7463 100644 --- a/console/src/locales/zh-TW.yaml +++ b/console/src/locales/zh-TW.yaml @@ -854,6 +854,7 @@ core: title: 用戶詳情 tabs: detail: 詳情 + notification-preferences: 通知配置 pat: 個人令牌 actions: update_profile: @@ -929,6 +930,9 @@ core: help: 留空代表永不過期 description: label: 描述 + notification-preferences: + fields: + type: 通知類型 role: title: 角色 common: diff --git a/console/src/modules/system/users/UserDetail.vue b/console/src/modules/system/users/UserDetail.vue index 33915d7df3..83540c7e1f 100644 --- a/console/src/modules/system/users/UserDetail.vue +++ b/console/src/modules/system/users/UserDetail.vue @@ -32,6 +32,7 @@ import { markRaw } from "vue"; import DetailTab from "./tabs/Detail.vue"; import PersonalAccessTokensTab from "./tabs/PersonalAccessTokens.vue"; import { useRouteQuery } from "@vueuse/router"; +import NotificationPreferences from "./tabs/NotificationPreferences.vue"; const { currentUserHasPermission } = usePermission(); const userStore = useUserStore(); @@ -111,11 +112,18 @@ const tabs = computed((): UserTab[] => { component: markRaw(DetailTab), priority: 10, }, + { + id: "notification-preferences", + label: t("core.user.detail.tabs.notification-preferences"), + component: markRaw(NotificationPreferences), + priority: 20, + hidden: !isCurrentUser.value, + }, { id: "pat", label: t("core.user.detail.tabs.pat"), component: markRaw(PersonalAccessTokensTab), - priority: 20, + priority: 30, hidden: !isCurrentUser.value, }, ]; diff --git a/console/src/modules/system/users/tabs/NotificationPreferences.vue b/console/src/modules/system/users/tabs/NotificationPreferences.vue new file mode 100644 index 0000000000..ca3d838781 --- /dev/null +++ b/console/src/modules/system/users/tabs/NotificationPreferences.vue @@ -0,0 +1,152 @@ + + +