diff --git a/component/api/pom.xml b/component/api/pom.xml index cc1963c8843..9c3d336908d 100644 --- a/component/api/pom.xml +++ b/component/api/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component-api Meeds:: PLF:: Social API diff --git a/component/common/pom.xml b/component/common/pom.xml index d997d1baeb1..e1351d9a3d6 100644 --- a/component/common/pom.xml +++ b/component/common/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT io.meeds.social social-component-common diff --git a/component/core/pom.xml b/component/core/pom.xml index 916070138fc..40f37f5c373 100644 --- a/component/core/pom.xml +++ b/component/core/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component-core Meeds:: PLF:: Social Core Component diff --git a/component/core/src/main/java/io/meeds/social/space/plugin/SpaceCategoryPlugin.java b/component/core/src/main/java/io/meeds/social/space/plugin/SpaceCategoryPlugin.java index de837609cbd..637406f0fc3 100644 --- a/component/core/src/main/java/io/meeds/social/space/plugin/SpaceCategoryPlugin.java +++ b/component/core/src/main/java/io/meeds/social/space/plugin/SpaceCategoryPlugin.java @@ -20,11 +20,17 @@ import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.provider.SpaceIdentityProvider; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.space.model.Space; import org.exoplatform.social.core.space.spi.SpaceService; +import io.meeds.social.category.model.CategoryObject; import io.meeds.social.category.plugin.CategoryPlugin; @Component @@ -35,6 +41,9 @@ public class SpaceCategoryPlugin implements CategoryPlugin { @Autowired private SpaceService spaceService; + @Autowired + private IdentityManager identityManager; + @Override public String getType() { return OBJECT_TYPE; @@ -55,4 +64,63 @@ public List getCategoryIds() { return spaceService.getSpaceCategoryIds(); } + /** + * Normalizes the identifier carried by the incoming {@link CategoryObject} to + * the canonical space technical id ({@link Space#getSpaceId()}) before the + * link is stored, read back or broadcast. Category links are stored generically + * on {@code (type, id)}, but the space UI/REST relies on the denormalized + * {@code space.getCategoryIds()} that is only kept in sync by + * {@code CategoryLinkModifiedListener}, whose SPACE branch resolves the space + * through {@code spaceService.getSpaceById(object.getId())} and therefore + * requires the technical id. Callers (e.g. the MCP tooling) may pass the space + * identity id or its pretty name instead; normalizing here makes the write, the + * read-back and the denormalization listener all agree whichever id form was + * supplied. + * + * @param categoryObject The incoming {@link CategoryObject} whose id may be a + * space technical id, a social {@link Identity} id or a pretty name + * @return A {@link CategoryObject} carrying the canonical space technical id, + * or the object unchanged when the id cannot be resolved to a space + **/ + @Override + public CategoryObject getObject(CategoryObject categoryObject) { + Space space = resolveSpace(categoryObject.getId()); + if (space == null) { + return categoryObject; + } + return new CategoryObject(categoryObject.getType(), + String.valueOf(space.getSpaceId()), + categoryObject.getParentId(), + categoryObject.getSpaceId()); + } + + /** + * Resolves a {@link Space} from any of the id forms a space can be referenced + * by across the platform: a space technical id, a social {@link Identity} id + * (mapped to its space through the space identity provider's remote id, i.e. + * the space pretty name) or a pretty name. This mirrors the space resolution + * idiom used by {@code SpacePermanentLinkPlugin#getDirectAccessUrl}. + * + * @param id A space technical id, social {@link Identity} id or pretty name + * @return The resolved {@link Space}, or {@code null} when none matches + **/ + private Space resolveSpace(String id) { + if (StringUtils.isBlank(id)) { + return null; + } + if (StringUtils.isNumeric(id)) { + Space space = spaceService.getSpaceById(id); + if (space != null) { + return space; + } + // Fall back to a social Identity id pointing at the space + Identity identity = identityManager.getIdentity(id); + if (identity != null && SpaceIdentityProvider.NAME.equals(identity.getProviderId())) { + return spaceService.getSpaceByPrettyName(identity.getRemoteId()); + } + return null; + } + return spaceService.getSpaceByPrettyName(id); + } + } diff --git a/component/core/src/test/java/io/meeds/social/category/service/CategoryLinkServiceTest.java b/component/core/src/test/java/io/meeds/social/category/service/CategoryLinkServiceTest.java index b7690bf8412..70156f39069 100644 --- a/component/core/src/test/java/io/meeds/social/category/service/CategoryLinkServiceTest.java +++ b/component/core/src/test/java/io/meeds/social/category/service/CategoryLinkServiceTest.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.Locale; +import org.apache.commons.collections4.CollectionUtils; import org.junit.Test; import org.exoplatform.commons.ObjectAlreadyExistsException; @@ -129,4 +130,30 @@ public void testUnLink() { assertFalse(categoryLinkService.isLinked(rootCategory.getId(), object)); } + @Test + @SneakyThrows + public void testLinkBySpaceIdentityIdDenormalizesCategoryIds() { + Space space = new Space(); + space.setRegistration(Space.OPEN); + space.setVisibility(Space.PUBLIC); + space = spaceService.createSpace(space, ROOT_USER); + + Category rootCategory = categoryService.getRootCategory(getAdminGroupIdentityId()); + + // Link the space using its social Identity id (how spaces are referenced + // across the platform) instead of its technical id, as the MCP tooling does + String spaceIdentityId = identityManager.getOrCreateSpaceIdentity(space.getPrettyName()).getId(); + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, spaceIdentityId, space.getSpaceId()); + categoryLinkService.link(rootCategory.getId(), object, ROOT_USER); + + // The link is stored/read-back under the normalized technical id + assertTrue(categoryLinkService.isLinked(rootCategory.getId(), object)); + + // And the denormalization listener kept space.categoryIds in sync + restartTransaction(); + Space updatedSpace = spaceService.getSpaceById(space.getId()); + assertTrue(CollectionUtils.isNotEmpty(updatedSpace.getCategoryIds()) + && updatedSpace.getCategoryIds().contains(rootCategory.getId())); + } + } diff --git a/component/core/src/test/java/io/meeds/social/space/plugin/SpaceCategoryPluginUnitTest.java b/component/core/src/test/java/io/meeds/social/space/plugin/SpaceCategoryPluginUnitTest.java new file mode 100644 index 00000000000..0cd4c936967 --- /dev/null +++ b/component/core/src/test/java/io/meeds/social/space/plugin/SpaceCategoryPluginUnitTest.java @@ -0,0 +1,120 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.space.plugin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import org.exoplatform.social.core.identity.model.Identity; +import org.exoplatform.social.core.identity.provider.SpaceIdentityProvider; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.space.model.Space; +import org.exoplatform.social.core.space.spi.SpaceService; + +import io.meeds.social.category.model.CategoryObject; + +@SpringBootTest(classes = { + SpaceCategoryPlugin.class, +}) +@RunWith(SpringRunner.class) +public class SpaceCategoryPluginUnitTest { + + private static final long SPACE_TECHNICAL_ID = 42l; + + private static final long SPACE_IDENTITY_ID = 74456l; + + private static final String SPACE_PRETTY_NAME = "prettyName"; + + @MockBean + private SpaceService spaceService; + + @MockBean + private IdentityManager identityManager; + + @Autowired + private SpaceCategoryPlugin spaceCategoryPlugin; + + @Test + public void testGetObjectNormalizesTechnicalId() { + Space space = newSpace(); + when(spaceService.getSpaceById(String.valueOf(SPACE_TECHNICAL_ID))).thenReturn(space); + + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, String.valueOf(SPACE_TECHNICAL_ID), 0l); + CategoryObject normalized = spaceCategoryPlugin.getObject(object); + + assertEquals(String.valueOf(SPACE_TECHNICAL_ID), normalized.getId()); + assertEquals(SpaceCategoryPlugin.OBJECT_TYPE, normalized.getType()); + } + + @Test + public void testGetObjectNormalizesIdentityIdToTechnicalId() { + Space space = newSpace(); + // The identity id is not a space technical id + when(spaceService.getSpaceById(String.valueOf(SPACE_IDENTITY_ID))).thenReturn(null); + Identity identity = mock(Identity.class); + when(identity.getProviderId()).thenReturn(SpaceIdentityProvider.NAME); + when(identity.getRemoteId()).thenReturn(SPACE_PRETTY_NAME); + when(identityManager.getIdentity(String.valueOf(SPACE_IDENTITY_ID))).thenReturn(identity); + when(spaceService.getSpaceByPrettyName(SPACE_PRETTY_NAME)).thenReturn(space); + + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, String.valueOf(SPACE_IDENTITY_ID), 0l); + CategoryObject normalized = spaceCategoryPlugin.getObject(object); + + assertEquals(String.valueOf(SPACE_TECHNICAL_ID), normalized.getId()); + } + + @Test + public void testGetObjectNormalizesPrettyNameToTechnicalId() { + Space space = newSpace(); + when(spaceService.getSpaceByPrettyName(SPACE_PRETTY_NAME)).thenReturn(space); + + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, SPACE_PRETTY_NAME, 0l); + CategoryObject normalized = spaceCategoryPlugin.getObject(object); + + assertEquals(String.valueOf(SPACE_TECHNICAL_ID), normalized.getId()); + } + + @Test + public void testGetObjectReturnsSameObjectWhenUnresolvable() { + when(spaceService.getSpaceById("unknown")).thenReturn(null); + when(spaceService.getSpaceByPrettyName("unknown")).thenReturn(null); + + CategoryObject object = new CategoryObject(SpaceCategoryPlugin.OBJECT_TYPE, "unknown", 0l); + CategoryObject normalized = spaceCategoryPlugin.getObject(object); + + assertSame(object, normalized); + } + + private Space newSpace() { + Space space = new Space(); + space.setId(String.valueOf(SPACE_TECHNICAL_ID)); + space.setPrettyName(SPACE_PRETTY_NAME); + return space; + } + +} diff --git a/component/notification/pom.xml b/component/notification/pom.xml index 2ca9bbaac27..52a57721001 100644 --- a/component/notification/pom.xml +++ b/component/notification/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component-notification Meeds:: PLF:: Social Notification Component diff --git a/component/oauth-auth/pom.xml b/component/oauth-auth/pom.xml index c7bb2a3e80f..18966e414f2 100644 --- a/component/oauth-auth/pom.xml +++ b/component/oauth-auth/pom.xml @@ -21,7 +21,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT 4.0.0 diff --git a/component/pom.xml b/component/pom.xml index b5976751ba8..c851837af89 100644 --- a/component/pom.xml +++ b/component/pom.xml @@ -22,7 +22,7 @@ social io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component pom diff --git a/component/service/pom.xml b/component/service/pom.xml index c6b52444ca4..166c7ac720d 100644 --- a/component/service/pom.xml +++ b/component/service/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-component-service Meeds:: PLF:: Social Service Component diff --git a/component/web/pom.xml b/component/web/pom.xml index 90866efe91e..e6ece9c6b6a 100644 --- a/component/web/pom.xml +++ b/component/web/pom.xml @@ -22,7 +22,7 @@ social-component io.meeds.social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT io.meeds.social social-component-web diff --git a/pom.xml b/pom.xml index ab09c7291e8..8d1b5df5275 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ io.meeds.social social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT pom Meeds:: PLF:: Social Meeds Social - Enterprise Social Networking @@ -45,8 +45,8 @@ - 7.3.x-SNAPSHOT - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT meeds-io diff --git a/webapp/pom.xml b/webapp/pom.xml index 4c6ca6afe37..bf1c5b46eb3 100644 --- a/webapp/pom.xml +++ b/webapp/pom.xml @@ -22,7 +22,7 @@ io.meeds.social social - 7.3.x-SNAPSHOT + 7.3.x-ai-contribution-SNAPSHOT social-webapp war diff --git a/webapp/src/main/webapp/vue-apps/general-settings/components/branding/drawers/PageStylingDrawer.vue b/webapp/src/main/webapp/vue-apps/general-settings/components/branding/drawers/PageStylingDrawer.vue index 3835d8e80f5..4a599ece67d 100644 --- a/webapp/src/main/webapp/vue-apps/general-settings/components/branding/drawers/PageStylingDrawer.vue +++ b/webapp/src/main/webapp/vue-apps/general-settings/components/branding/drawers/PageStylingDrawer.vue @@ -121,7 +121,7 @@ export default { drawer: false, backgroundProperties: null, defaultCustomPageWidth: '1320', - defaultPageBackground: '#F0F0F0', + defaultPageBackground: '#F0F0F0FF', pageWidth: null, borderRadius: null, defaultPageStylingProperties: null, @@ -164,7 +164,7 @@ export default { methods: { init() { this.backgroundProperties = { - backgroundColor: this.pageStylingProperties?.pageBackgroundColor || this.defaultPageBackground, + backgroundColor: this.normalizeBackgroundColor(this.pageStylingProperties?.pageBackgroundColor) || this.defaultPageBackground, backgroundPosition: this.pageStylingProperties?.pageBackgroundPosition || null, background: this.pageStylingProperties?.pageBackground || null, backgroundRepeat: this.pageStylingProperties?.pageBackgroundRepeat || null, @@ -205,6 +205,12 @@ export default { this.reset(); this.$refs.drawer.close(); }, + normalizeBackgroundColor(color) { + if (!color) { + return color; + } + return color.length === 7 ? `${color}FF` : color; + }, getPageBackgroundEffect() { const effect = this.pageStylingProperties?.pageBackgroundEffect; if (!effect || effect === 'none') { diff --git a/webapp/src/main/webapp/vue-apps/general-settings/components/branding/form/BackgroundInput.vue b/webapp/src/main/webapp/vue-apps/general-settings/components/branding/form/BackgroundInput.vue index 94a2f0dfa5b..6aa2371083e 100644 --- a/webapp/src/main/webapp/vue-apps/general-settings/components/branding/form/BackgroundInput.vue +++ b/webapp/src/main/webapp/vue-apps/general-settings/components/branding/form/BackgroundInput.vue @@ -225,7 +225,7 @@ export default { this.backgroundGradientFrom = null; this.backgroundGradientTo = null; } else if (this.choice === 'gradient') { - if (this.branding.backgroundEffect) { + if (this.branding.backgroundEffect && this.branding.backgroundEffect.startsWith('linear-gradient(')) { this.backgroundGradientFrom = this.branding.backgroundEffect.replace('linear-gradient(', '').split(',')[0].trim(); this.backgroundGradientTo = this.branding.backgroundEffect.replace('linear-gradient(', '').split(',')[1].replace(/\)$/g, '').trim(); } else { @@ -253,7 +253,7 @@ export default { this.backgroundImageStyle = this.branding.backgroundRepeat; } } - if (this.branding.backgroundEffect) { + if (this.branding.backgroundEffect && this.branding.backgroundEffect.startsWith('linear-gradient(')) { this.choice = 'gradient'; this.backgroundGradientFrom = this.branding.backgroundEffect.replace('linear-gradient(', '').split(',')[0].trim(); this.backgroundGradientTo = this.branding.backgroundEffect.replace('linear-gradient(', '').split(',')[1].replace(/\)$/g, '').trim();