From 5208b5c9253bd33d152549a988b2f1b5ee7e1d8f Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 30 Nov 2023 18:46:08 +0800 Subject: [PATCH 01/35] Fix the problem of incorrect old data passed to watcher during updates (#4959) #### What type of PR is this? /kind bug /area core /milestone 2.11.0 #### What this PR does / why we need it: This PR resolves the problem of incorrect old data passed to watcher during updates. As shown in the following line, the old value should be `old` instead of `extension` from outside. https://github.com/halo-dev/halo/blob/7a84f553005b2d8047ccdb0acf473693384a7b51/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java#L172 #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../run/halo/app/extension/JsonExtension.java | 18 ++++ .../ReactiveExtensionClientImpl.java | 86 +++++++++++-------- .../run/halo/app/extension/FakeExtension.java | 13 +++ .../ReactiveExtensionClientTest.java | 38 ++++++++ 4 files changed, 118 insertions(+), 37 deletions(-) diff --git a/api/src/main/java/run/halo/app/extension/JsonExtension.java b/api/src/main/java/run/halo/app/extension/JsonExtension.java index 67ef64fc89..2bcfec3cd7 100644 --- a/api/src/main/java/run/halo/app/extension/JsonExtension.java +++ b/api/src/main/java/run/halo/app/extension/JsonExtension.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.time.Instant; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -117,6 +118,23 @@ public MetadataOperator getMetadataOrCreate() { return new ObjectNodeMetadata(metadataNode); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JsonExtension that = (JsonExtension) o; + return Objects.equals(objectNode, that.objectNode); + } + + @Override + public int hashCode() { + return Objects.hash(objectNode); + } + class ObjectNodeMetadata implements MetadataOperator { private final ObjectNode objectNode; diff --git a/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java b/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java index a495356809..48eda2f221 100644 --- a/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java +++ b/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java @@ -4,12 +4,13 @@ import static org.springframework.util.StringUtils.hasText; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.time.Duration; import java.time.Instant; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.util.Predicates; @@ -137,42 +138,31 @@ public Mono create(E extension) { @SuppressWarnings("unchecked") public Mono update(E extension) { // Refactor the atomic reference if we have a better solution. - final var statusChangeOnly = new AtomicBoolean(false); - return getLatest(extension) - .map(old -> new JsonExtension(objectMapper, old)) - .flatMap(oldJsonExt -> { - var newJsonExt = new JsonExtension(objectMapper, extension); - // reset some mandatory fields - var oldMetadata = oldJsonExt.getMetadata(); - var newMetadata = newJsonExt.getMetadata(); - newMetadata.setCreationTimestamp(oldMetadata.getCreationTimestamp()); - newMetadata.setGenerateName(oldMetadata.getGenerateName()); - - var oldObjectNode = oldJsonExt.getInternal().deepCopy(); - var newObjectNode = newJsonExt.getInternal().deepCopy(); - if (Objects.equals(oldObjectNode, newObjectNode)) { - // if no data were changed, just skip updating. - return Mono.empty(); - } - // check status is changed - oldObjectNode.remove("status"); - newObjectNode.remove("status"); - if (Objects.equals(oldObjectNode, newObjectNode)) { - statusChangeOnly.set(true); - } - return Mono.just(newJsonExt); - }) - .map(converter::convertTo) - .flatMap(extensionStore -> client.update(extensionStore.getName(), - extensionStore.getVersion(), - extensionStore.getData())) - .map(updated -> converter.convertFrom((Class) extension.getClass(), updated)) - .doOnNext(updated -> { - if (!statusChangeOnly.get()) { - watchers.onUpdate(extension, updated); - } - }) - .switchIfEmpty(Mono.defer(() -> Mono.just(extension))); + return getLatest(extension).flatMap(old -> { + var oldJsonExt = new JsonExtension(objectMapper, old); + var newJsonExt = new JsonExtension(objectMapper, extension); + // reset some mandatory fields + var oldMetadata = oldJsonExt.getMetadata(); + var newMetadata = newJsonExt.getMetadata(); + newMetadata.setCreationTimestamp(oldMetadata.getCreationTimestamp()); + newMetadata.setGenerateName(oldMetadata.getGenerateName()); + + if (Objects.equals(oldJsonExt, newJsonExt)) { + // skip updating if not data changed. + return Mono.just(extension); + } + + var onlyStatusChanged = + isOnlyStatusChanged(oldJsonExt.getInternal(), newJsonExt.getInternal()); + + var store = this.converter.convertTo(newJsonExt); + var updated = client.update(store.getName(), store.getVersion(), store.getData()) + .map(ext -> converter.convertFrom((Class) extension.getClass(), ext)); + if (!onlyStatusChanged) { + updated = updated.doOnNext(ext -> watchers.onUpdate(old, ext)); + } + return updated; + }); } private Mono getLatest(Extension extension) { @@ -199,4 +189,26 @@ public void watch(Watcher watcher) { this.watchers.addWatcher(watcher); } + private static boolean isOnlyStatusChanged(ObjectNode oldNode, ObjectNode newNode) { + if (Objects.equals(oldNode, newNode)) { + return false; + } + // WARNING!!! + // Do not edit the ObjectNode + var oldFields = new HashSet(); + var newFields = new HashSet(); + oldNode.fieldNames().forEachRemaining(oldFields::add); + newNode.fieldNames().forEachRemaining(newFields::add); + oldFields.remove("status"); + newFields.remove("status"); + if (!Objects.equals(oldFields, newFields)) { + return false; + } + for (var field : oldFields) { + if (!Objects.equals(oldNode.get(field), newNode.get(field))) { + return false; + } + } + return true; + } } diff --git a/application/src/test/java/run/halo/app/extension/FakeExtension.java b/application/src/test/java/run/halo/app/extension/FakeExtension.java index d0e5cb03bf..5933c37a3c 100644 --- a/application/src/test/java/run/halo/app/extension/FakeExtension.java +++ b/application/src/test/java/run/halo/app/extension/FakeExtension.java @@ -1,12 +1,21 @@ package run.halo.app.extension; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + @GVK(group = "fake.halo.run", version = "v1alpha1", kind = "Fake", plural = "fakes", singular = "fake") +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class FakeExtension extends AbstractExtension { + private FakeStatus status = new FakeStatus(); + public static FakeExtension createFake(String name) { var metadata = new Metadata(); metadata.setName(name); @@ -15,4 +24,8 @@ public static FakeExtension createFake(String name) { return fake; } + @Data + public static class FakeStatus { + private String state; + } } diff --git a/application/src/test/java/run/halo/app/extension/ReactiveExtensionClientTest.java b/application/src/test/java/run/halo/app/extension/ReactiveExtensionClientTest.java index 7e60e74f6d..0f83fbe092 100644 --- a/application/src/test/java/run/halo/app/extension/ReactiveExtensionClientTest.java +++ b/application/src/test/java/run/halo/app/extension/ReactiveExtensionClientTest.java @@ -455,6 +455,37 @@ void shouldNotUpdateIfExtensionNotChange() { verify(storeClient, never()).update(any(), any(), any()); } + @Test + void shouldUpdateIfExtensionStatusChangedOnly() { + var fake = createFakeExtension("fake", 2L); + fake.getStatus().setState("new-state"); + var storeName = "/registry/fake.halo.run/fakes/fake"; + when(converter.convertTo(any())).thenReturn( + createExtensionStore(storeName, 2L)); + when(storeClient.update(any(), any(), any())).thenReturn( + Mono.just(createExtensionStore(storeName, 2L))); + when(storeClient.fetchByName(storeName)).thenReturn( + Mono.just(createExtensionStore(storeName, 1L))); + + var oldFake = createFakeExtension("fake", 2L); + oldFake.getStatus().setState("old-state"); + + var updatedFake = createFakeExtension("fake", 3L); + when(converter.convertFrom(same(FakeExtension.class), any())) + .thenReturn(oldFake) + .thenReturn(updatedFake); + + StepVerifier.create(client.update(fake)) + .expectNext(updatedFake) + .verifyComplete(); + + verify(storeClient).fetchByName(storeName); + verify(converter).convertTo(isA(JsonExtension.class)); + verify(converter, times(2)).convertFrom(same(FakeExtension.class), any()); + verify(storeClient) + .update(eq("/registry/fake.halo.run/fakes/fake"), eq(2L), any()); + } + @Test void shouldUpdateUnstructuredSuccessfully() throws JsonProcessingException { var fake = createUnstructured(); @@ -539,6 +570,13 @@ void shouldNotWatchOnUpdateIfExtensionNotChange() { verify(watcher, never()).onUpdate(any(), any()); } + @Test + void shouldNotWatchOnUpdateIfExtensionStatusChangeOnly() { + shouldUpdateIfExtensionStatusChangedOnly(); + + verify(watcher, never()).onUpdate(any(), any()); + } + @Test void shouldWatchOnDeleteSuccessfully() { doNothing().when(watcher).onDelete(any()); From 5e76da018dc916d056447203b1a917a9c6409433 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Thu, 30 Nov 2023 18:56:10 +0800 Subject: [PATCH 02/35] feat: refine i18n for uc (#4957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /area console /kind improvement /milestone 2.11.x #### What this PR does / why we need it: 完善个人中心相关页面的 i18n。 #### Special notes for your reviewer: 测试各个语言的个人中心相关页面。 #### Does this PR introduce a user-facing change? ```release-note 完善个人中心相关页面的 i18n。 ``` --- console/console-src/layouts/BasicLayout.vue | 3 +- .../roles/components/RoleEditingModal.vue | 2 +- .../modules/system/users/UserDetail.vue | 2 +- .../users/widgets/NotificationWidget.vue | 4 +- .../packages/editor/src/locales/zh-CN.yaml | 2 +- .../src/components/user-avatar/UserAvatar.vue | 8 +- .../user-avatar/UserAvatarCropper.vue | 12 +- console/src/locales/en.yaml | 444 ++- console/src/locales/es.yaml | 2544 +++++++++-------- console/src/locales/zh-CN.yaml | 292 +- console/src/locales/zh-TW.yaml | 292 +- console/uc-src/layouts/BasicLayout.vue | 3 +- .../modules/contents/posts/PostEditor.vue | 4 +- .../modules/contents/posts/PostList.vue | 14 +- .../posts/components/PostListItem.vue | 12 +- .../posts/components/PostSettingEditModal.vue | 2 +- .../uc-src/modules/contents/posts/module.ts | 6 +- .../modules/notifications/Notifications.vue | 10 +- .../components/NotificationListItem.vue | 10 +- .../uc-src/modules/notifications/module.ts | 4 +- console/uc-src/modules/profile/Profile.vue | 10 +- .../profile/components/EmailVerifyModal.vue | 52 +- .../components/PasswordChangeModal.vue | 10 +- .../PersonalAccessTokenCreationModal.vue | 24 +- .../PersonalAccessTokenListItem.vue | 26 +- .../components/ProfileEditingModal.vue | 18 +- console/uc-src/modules/profile/module.ts | 4 +- .../uc-src/modules/profile/tabs/Detail.vue | 38 +- .../profile/tabs/NotificationPreferences.vue | 2 +- .../profile/tabs/PersonalAccessTokens.vue | 4 +- 30 files changed, 2155 insertions(+), 1703 deletions(-) diff --git a/console/console-src/layouts/BasicLayout.vue b/console/console-src/layouts/BasicLayout.vue index 60244afec4..31090de702 100644 --- a/console/console-src/layouts/BasicLayout.vue +++ b/console/console-src/layouts/BasicLayout.vue @@ -179,7 +179,7 @@ onMounted(() => {
@@ -188,6 +188,7 @@ onMounted(() => { />
diff --git a/console/console-src/modules/system/roles/components/RoleEditingModal.vue b/console/console-src/modules/system/roles/components/RoleEditingModal.vue index c093e5754c..35c8e8b823 100644 --- a/console/console-src/modules/system/roles/components/RoleEditingModal.vue +++ b/console/console-src/modules/system/roles/components/RoleEditingModal.vue @@ -161,7 +161,7 @@ const handleResetForm = () => { ] " type="text" - label="登录之后默认跳转位置" + :label="$t('core.role.editing_modal.fields.redirect_on_login')" >
diff --git a/console/console-src/modules/system/users/UserDetail.vue b/console/console-src/modules/system/users/UserDetail.vue index da3a3a6dfc..f7f607b5ab 100644 --- a/console/console-src/modules/system/users/UserDetail.vue +++ b/console/console-src/modules/system/users/UserDetail.vue @@ -123,7 +123,7 @@ function handleRouteToUC() { type="primary" @click="handleRouteToUC" > - 个人中心 + {{ $t("core.user.detail.actions.profile.title") }} diff --git a/console/console-src/modules/system/users/widgets/NotificationWidget.vue b/console/console-src/modules/system/users/widgets/NotificationWidget.vue index b9239e58a6..14d2880fce 100644 --- a/console/console-src/modules/system/users/widgets/NotificationWidget.vue +++ b/console/console-src/modules/system/users/widgets/NotificationWidget.vue @@ -44,7 +44,7 @@ function handleRouteToNotification(notification: Notification) {