mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-24 16:38:19 +00:00
feat(homepage): build announcements functionality on the new homepage (#13938)
This commit is contained in:
parent
2c4d629b53
commit
d2d9d36987
@ -203,6 +203,7 @@ import com.linkedin.datahub.graphql.resolvers.settings.applications.UpdateApplic
|
||||
import com.linkedin.datahub.graphql.resolvers.settings.docPropagation.DocPropagationSettingsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.settings.docPropagation.UpdateDocPropagationSettingsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.settings.user.UpdateCorpUserViewsSettingsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.settings.user.UpdateUserHomePageSettingsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.settings.view.GlobalViewsSettingsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.settings.view.UpdateGlobalViewsSettingsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.siblings.SiblingsSearchResolver;
|
||||
@ -1292,6 +1293,9 @@ public class GmsGraphQLEngine {
|
||||
.dataFetcher(
|
||||
"updateCorpUserViewsSettings",
|
||||
new UpdateCorpUserViewsSettingsResolver(this.settingsService))
|
||||
.dataFetcher(
|
||||
"updateUserHomePageSettings",
|
||||
new UpdateUserHomePageSettingsResolver(this.settingsService))
|
||||
.dataFetcher(
|
||||
"updateLineage",
|
||||
new UpdateLineageResolver(this.entityService, this.lineageService))
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.settings.user;
|
||||
|
||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
|
||||
|
||||
import com.linkedin.common.UrnArray;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
|
||||
import com.linkedin.datahub.graphql.generated.UpdateUserHomePageSettingsInput;
|
||||
import com.linkedin.identity.CorpUserAppearanceSettings;
|
||||
import com.linkedin.identity.CorpUserHomePageSettings;
|
||||
import com.linkedin.identity.CorpUserSettings;
|
||||
import com.linkedin.metadata.service.SettingsService;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.annotation.Nonnull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/** Resolver responsible for updating the authenticated user's HomePage-specific settings. */
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class UpdateUserHomePageSettingsResolver implements DataFetcher<CompletableFuture<Boolean>> {
|
||||
|
||||
private final SettingsService _settingsService;
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
|
||||
final QueryContext context = environment.getContext();
|
||||
final UpdateUserHomePageSettingsInput input =
|
||||
bindArgument(environment.getArgument("input"), UpdateUserHomePageSettingsInput.class);
|
||||
|
||||
return GraphQLConcurrencyUtils.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
|
||||
final Urn userUrn = UrnUtils.getUrn(context.getActorUrn());
|
||||
|
||||
final CorpUserSettings maybeSettings =
|
||||
_settingsService.getCorpUserSettings(context.getOperationContext(), userUrn);
|
||||
|
||||
final CorpUserSettings newSettings =
|
||||
maybeSettings == null
|
||||
? new CorpUserSettings()
|
||||
.setAppearance(
|
||||
new CorpUserAppearanceSettings().setShowSimplifiedHomepage(false))
|
||||
: maybeSettings;
|
||||
|
||||
// Patch the new corp user settings. This does a R-M-F.
|
||||
updateCorpUserSettings(newSettings, input);
|
||||
|
||||
_settingsService.updateCorpUserSettings(
|
||||
context.getOperationContext(), userUrn, newSettings);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to perform user home page settings update against input {}, {}",
|
||||
input.toString(),
|
||||
e.getMessage());
|
||||
throw new RuntimeException(
|
||||
String.format(
|
||||
"Failed to perform update to user home page settings against input %s",
|
||||
input.toString()),
|
||||
e);
|
||||
}
|
||||
},
|
||||
this.getClass().getSimpleName(),
|
||||
"get");
|
||||
}
|
||||
|
||||
private static void updateCorpUserSettings(
|
||||
@Nonnull final CorpUserSettings settings,
|
||||
@Nonnull final UpdateUserHomePageSettingsInput input) {
|
||||
final CorpUserHomePageSettings newHomePageSettings =
|
||||
settings.hasHomePage() ? settings.getHomePage() : new CorpUserHomePageSettings();
|
||||
updateCorpUserHomePageSettings(newHomePageSettings, input);
|
||||
settings.setHomePage(newHomePageSettings);
|
||||
}
|
||||
|
||||
private static void updateCorpUserHomePageSettings(
|
||||
@Nonnull final CorpUserHomePageSettings settings,
|
||||
@Nonnull final UpdateUserHomePageSettingsInput input) {
|
||||
|
||||
if (input.getPageTemplate() != null) {
|
||||
settings.setPageTemplate(UrnUtils.getUrn(input.getPageTemplate()));
|
||||
}
|
||||
|
||||
// Append to the list of existing dismissed announcements
|
||||
if (input.getNewDismissedAnnouncements() != null) {
|
||||
List<Urn> dismissedAnnouncements =
|
||||
settings.hasDismissedAnnouncements()
|
||||
? new ArrayList<>(settings.getDismissedAnnouncements())
|
||||
: new ArrayList<>();
|
||||
|
||||
for (String announcement : input.getNewDismissedAnnouncements()) {
|
||||
try {
|
||||
Urn urn = Urn.createFromString(announcement);
|
||||
if (!dismissedAnnouncements.contains(urn)) {
|
||||
dismissedAnnouncements.add(urn);
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
log.error("Invalid URN: ", announcement, e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
settings.setDismissedAnnouncements(new UrnArray(dismissedAnnouncements));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,11 +11,14 @@ import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.featureflags.FeatureFlags;
|
||||
import com.linkedin.datahub.graphql.generated.CorpUser;
|
||||
import com.linkedin.datahub.graphql.generated.CorpUserAppearanceSettings;
|
||||
import com.linkedin.datahub.graphql.generated.CorpUserHomePageSettings;
|
||||
import com.linkedin.datahub.graphql.generated.CorpUserProperties;
|
||||
import com.linkedin.datahub.graphql.generated.CorpUserViewsSettings;
|
||||
import com.linkedin.datahub.graphql.generated.DataHubPageTemplate;
|
||||
import com.linkedin.datahub.graphql.generated.DataHubView;
|
||||
import com.linkedin.datahub.graphql.generated.EntityType;
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper;
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper;
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper;
|
||||
import com.linkedin.datahub.graphql.types.form.FormsMapper;
|
||||
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertiesMapper;
|
||||
@ -30,6 +33,8 @@ import com.linkedin.identity.CorpUserSettings;
|
||||
import com.linkedin.identity.CorpUserStatus;
|
||||
import com.linkedin.metadata.key.CorpUserKey;
|
||||
import com.linkedin.structured.StructuredProperties;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@ -118,6 +123,11 @@ public class CorpUserMapper {
|
||||
result.setViews(mapCorpUserViewsSettings(corpUserSettings.getViews()));
|
||||
}
|
||||
|
||||
// Map Home page Settings.
|
||||
if (corpUserSettings.hasHomePage()) {
|
||||
result.setHomePage(mapCorpUserHomePageSettings(corpUserSettings.getHomePage()));
|
||||
}
|
||||
|
||||
corpUser.setSettings(result);
|
||||
}
|
||||
|
||||
@ -154,6 +164,27 @@ public class CorpUserMapper {
|
||||
return viewsResult;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private CorpUserHomePageSettings mapCorpUserHomePageSettings(
|
||||
@Nonnull final com.linkedin.identity.CorpUserHomePageSettings homePageSettings) {
|
||||
CorpUserHomePageSettings result = new CorpUserHomePageSettings();
|
||||
|
||||
if (homePageSettings.hasPageTemplate()) {
|
||||
result.setPageTemplate(
|
||||
(DataHubPageTemplate) UrnToEntityMapper.map(null, homePageSettings.getPageTemplate()));
|
||||
}
|
||||
|
||||
if (homePageSettings.hasDismissedAnnouncements()) {
|
||||
List<String> dismissedUrnStrings =
|
||||
homePageSettings.getDismissedAnnouncements().stream()
|
||||
.map(Urn::toString)
|
||||
.collect(Collectors.toList());
|
||||
result.setDismissedAnnouncementUrns(dismissedUrnStrings);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void mapCorpUserKey(@Nonnull CorpUser corpUser, @Nonnull DataMap dataMap) {
|
||||
CorpUserKey corpUserKey = new CorpUserKey(dataMap);
|
||||
corpUser.setUsername(corpUserKey.getUsername());
|
||||
|
||||
@ -768,6 +768,11 @@ type Mutation {
|
||||
"""
|
||||
updateCorpUserViewsSettings(input: UpdateCorpUserViewsSettingsInput!): Boolean
|
||||
|
||||
"""
|
||||
Update the HomePage-related settings for a user.
|
||||
"""
|
||||
updateUserHomePageSettings(input: UpdateUserHomePageSettingsInput!): Boolean
|
||||
|
||||
"""
|
||||
Update a user setting
|
||||
"""
|
||||
@ -4239,6 +4244,11 @@ type CorpUserHomePageSettings {
|
||||
The default page template for the User.
|
||||
"""
|
||||
pageTemplate: DataHubPageTemplate
|
||||
|
||||
"""
|
||||
List of urns of the announcements dismissed by the User.
|
||||
"""
|
||||
dismissedAnnouncementUrns: [String]
|
||||
}
|
||||
|
||||
"""
|
||||
@ -12446,6 +12456,21 @@ input UpdateCorpUserViewsSettingsInput {
|
||||
defaultView: String
|
||||
}
|
||||
|
||||
"""
|
||||
Input required to update a user's home page settings.
|
||||
"""
|
||||
input UpdateUserHomePageSettingsInput {
|
||||
"""
|
||||
The URN of the page template to be rendered on the home page for the user.
|
||||
"""
|
||||
pageTemplate: String
|
||||
|
||||
"""
|
||||
The list of urns of announcement posts dismissed by the user.
|
||||
"""
|
||||
newDismissedAnnouncements: [String]
|
||||
}
|
||||
|
||||
"""
|
||||
Information required to render an embedded version of an asset
|
||||
"""
|
||||
|
||||
@ -0,0 +1,197 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.settings.user;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
import com.linkedin.common.UrnArray;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.UpdateUserHomePageSettingsInput;
|
||||
import com.linkedin.identity.CorpUserAppearanceSettings;
|
||||
import com.linkedin.identity.CorpUserHomePageSettings;
|
||||
import com.linkedin.identity.CorpUserSettings;
|
||||
import com.linkedin.metadata.service.SettingsService;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class UpdateUserHomePageSettingsResolverTest {
|
||||
|
||||
private static final Urn TEST_USER_URN = UrnUtils.getUrn("urn:li:corpuser:test");
|
||||
private static final UpdateUserHomePageSettingsInput TEST_INPUT =
|
||||
new UpdateUserHomePageSettingsInput();
|
||||
private static final UpdateUserHomePageSettingsInput TEST_INPUT_WITH_TEMPLATE =
|
||||
new UpdateUserHomePageSettingsInput();
|
||||
private static final UpdateUserHomePageSettingsInput TEST_INPUT_NO_DISMISSED =
|
||||
new UpdateUserHomePageSettingsInput();
|
||||
|
||||
static {
|
||||
TEST_INPUT.setNewDismissedAnnouncements(Arrays.asList("urn:li:post:a1", "urn:li:post:a2"));
|
||||
TEST_INPUT_WITH_TEMPLATE.setPageTemplate("urn:li:pageTemplate:homepage");
|
||||
TEST_INPUT_WITH_TEMPLATE.setNewDismissedAnnouncements(
|
||||
Collections.singletonList("urn:li:post:a3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSettingsWithNoExistingSettings() throws Exception {
|
||||
SettingsService mockService = initSettingsService(TEST_USER_URN, null);
|
||||
UpdateUserHomePageSettingsResolver resolver =
|
||||
new UpdateUserHomePageSettingsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString());
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
CorpUserSettings expectedSettings =
|
||||
new CorpUserSettings()
|
||||
.setAppearance(new CorpUserAppearanceSettings().setShowSimplifiedHomepage(false))
|
||||
.setHomePage(
|
||||
new CorpUserHomePageSettings()
|
||||
.setDismissedAnnouncements(
|
||||
new UrnArray(
|
||||
Arrays.asList(
|
||||
Urn.createFromString("urn:li:post:a1"),
|
||||
Urn.createFromString("urn:li:post:a2")))));
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1))
|
||||
.updateCorpUserSettings(any(), Mockito.eq(TEST_USER_URN), Mockito.eq(expectedSettings));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSettingsWithExistingSettingsAndAnnouncements() throws Exception {
|
||||
CorpUserHomePageSettings existingHomePage =
|
||||
new CorpUserHomePageSettings()
|
||||
.setDismissedAnnouncements(
|
||||
new UrnArray(Collections.singletonList(Urn.createFromString("urn:li:post:a1"))));
|
||||
CorpUserSettings existingSettings =
|
||||
new CorpUserSettings()
|
||||
.setAppearance(new CorpUserAppearanceSettings().setShowSimplifiedHomepage(true))
|
||||
.setHomePage(existingHomePage);
|
||||
|
||||
SettingsService mockService = initSettingsService(TEST_USER_URN, existingSettings);
|
||||
UpdateUserHomePageSettingsResolver resolver =
|
||||
new UpdateUserHomePageSettingsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString());
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
CorpUserSettings expectedSettings =
|
||||
new CorpUserSettings()
|
||||
.setAppearance(new CorpUserAppearanceSettings().setShowSimplifiedHomepage(true))
|
||||
.setHomePage(
|
||||
new CorpUserHomePageSettings()
|
||||
.setDismissedAnnouncements(
|
||||
new UrnArray(
|
||||
Arrays.asList(
|
||||
Urn.createFromString("urn:li:post:a1"),
|
||||
Urn.createFromString("urn:li:post:a2")))));
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1))
|
||||
.updateCorpUserSettings(any(), Mockito.eq(TEST_USER_URN), Mockito.eq(expectedSettings));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSettingsWithPageTemplate() throws Exception {
|
||||
SettingsService mockService = initSettingsService(TEST_USER_URN, null);
|
||||
UpdateUserHomePageSettingsResolver resolver =
|
||||
new UpdateUserHomePageSettingsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString());
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_WITH_TEMPLATE);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
CorpUserSettings expectedSettings =
|
||||
new CorpUserSettings()
|
||||
.setAppearance(new CorpUserAppearanceSettings().setShowSimplifiedHomepage(false))
|
||||
.setHomePage(
|
||||
new CorpUserHomePageSettings()
|
||||
.setPageTemplate(UrnUtils.getUrn("urn:li:pageTemplate:homepage"))
|
||||
.setDismissedAnnouncements(
|
||||
new UrnArray(
|
||||
Collections.singletonList(Urn.createFromString("urn:li:post:a3")))));
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1))
|
||||
.updateCorpUserSettings(any(), Mockito.eq(TEST_USER_URN), Mockito.eq(expectedSettings));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSettingsWithNoDismissedAnnouncements() throws Exception {
|
||||
SettingsService mockService = initSettingsService(TEST_USER_URN, null);
|
||||
UpdateUserHomePageSettingsResolver resolver =
|
||||
new UpdateUserHomePageSettingsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString());
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_NO_DISMISSED);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
CorpUserSettings expectedSettings =
|
||||
new CorpUserSettings()
|
||||
.setAppearance(new CorpUserAppearanceSettings().setShowSimplifiedHomepage(false))
|
||||
.setHomePage(new CorpUserHomePageSettings());
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1))
|
||||
.updateCorpUserSettings(any(), Mockito.eq(TEST_USER_URN), Mockito.eq(expectedSettings));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCorpUserSettingsException() throws Exception {
|
||||
SettingsService mockService = Mockito.mock(SettingsService.class);
|
||||
Mockito.doThrow(RuntimeException.class)
|
||||
.when(mockService)
|
||||
.getCorpUserSettings(any(), Mockito.eq(TEST_USER_URN));
|
||||
|
||||
UpdateUserHomePageSettingsResolver resolver =
|
||||
new UpdateUserHomePageSettingsResolver(mockService);
|
||||
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString());
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateCorpUserSettingsException() throws Exception {
|
||||
SettingsService mockService = initSettingsService(TEST_USER_URN, null);
|
||||
Mockito.doThrow(RuntimeException.class)
|
||||
.when(mockService)
|
||||
.updateCorpUserSettings(
|
||||
any(), Mockito.eq(TEST_USER_URN), Mockito.any(CorpUserSettings.class));
|
||||
|
||||
UpdateUserHomePageSettingsResolver resolver =
|
||||
new UpdateUserHomePageSettingsResolver(mockService);
|
||||
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString());
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
}
|
||||
|
||||
private static SettingsService initSettingsService(Urn user, CorpUserSettings existingSettings) {
|
||||
SettingsService mockService = Mockito.mock(SettingsService.class);
|
||||
Mockito.when(mockService.getCorpUserSettings(any(), Mockito.eq(user)))
|
||||
.thenReturn(existingSettings);
|
||||
return mockService;
|
||||
}
|
||||
}
|
||||
@ -129,6 +129,7 @@ export const user1 = {
|
||||
__typename: 'CorpUserSettings',
|
||||
appearance: { __typename: 'CorpUserAppearanceSettings', showSimplifiedHomepage: false, showThemeV2: false },
|
||||
views: { __typename: 'CorpUserViewSettings', defaultView: null },
|
||||
homePage: null,
|
||||
},
|
||||
editableInfo: null,
|
||||
properties: null,
|
||||
@ -201,6 +202,7 @@ const user2 = {
|
||||
__typename: 'CorpUserSettings',
|
||||
appearance: { __typename: 'CorpUserAppearanceSettings', showSimplifiedHomepage: false, showThemeV2: false },
|
||||
views: { __typename: 'CorpUserViewSettings', defaultView: null },
|
||||
homePage: null,
|
||||
},
|
||||
editableInfo: null,
|
||||
info: null,
|
||||
|
||||
@ -15,6 +15,7 @@ export const cardDefaults: CardProps = {
|
||||
title: 'Title',
|
||||
iconAlignment: 'horizontal',
|
||||
isEmpty: false,
|
||||
isCardClickable: true,
|
||||
};
|
||||
|
||||
export const Card = ({
|
||||
@ -30,6 +31,8 @@ export const Card = ({
|
||||
maxWidth,
|
||||
height,
|
||||
isEmpty,
|
||||
style,
|
||||
isCardClickable = cardDefaults.isCardClickable,
|
||||
}: CardProps) => {
|
||||
return (
|
||||
<>
|
||||
@ -41,7 +44,14 @@ export const Card = ({
|
||||
</TitleContainer>
|
||||
</CardContainer>
|
||||
) : (
|
||||
<CardContainer hasButton={!!button} onClick={onClick} maxWidth={maxWidth} height={height} width={width}>
|
||||
<CardContainer
|
||||
isClickable={!!button && isCardClickable}
|
||||
onClick={onClick}
|
||||
maxWidth={maxWidth}
|
||||
height={height}
|
||||
width={width}
|
||||
style={style}
|
||||
>
|
||||
<Header iconAlignment={iconAlignment}>
|
||||
{icon}
|
||||
<TitleContainer>
|
||||
|
||||
@ -3,8 +3,8 @@ import styled from 'styled-components';
|
||||
import { colors, radius, spacing, typography } from '@src/alchemy-components/theme';
|
||||
import { IconAlignmentOptions } from '@src/alchemy-components/theme/config';
|
||||
|
||||
export const CardContainer = styled.div<{ hasButton?: boolean; width?: string; maxWidth?: string; height?: string }>(
|
||||
({ hasButton, width, maxWidth, height }) => ({
|
||||
export const CardContainer = styled.div<{ isClickable?: boolean; width?: string; maxWidth?: string; height?: string }>(
|
||||
({ isClickable, width, maxWidth, height }) => ({
|
||||
border: `1px solid ${colors.gray[100]}`,
|
||||
borderRadius: radius.lg,
|
||||
padding: spacing.md,
|
||||
@ -19,7 +19,7 @@ export const CardContainer = styled.div<{ hasButton?: boolean; width?: string; m
|
||||
width,
|
||||
height,
|
||||
|
||||
'&:hover': hasButton
|
||||
'&:hover': isClickable
|
||||
? {
|
||||
border: `1px solid ${({ theme }) => theme.styles['primary-color']}`,
|
||||
cursor: 'pointer',
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { IconAlignmentOptions } from '@src/alchemy-components/theme/config';
|
||||
|
||||
export interface CardProps {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
title: string | React.ReactNode;
|
||||
subTitle?: string | React.ReactNode;
|
||||
percent?: number;
|
||||
button?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
@ -13,4 +13,6 @@ export interface CardProps {
|
||||
maxWidth?: string;
|
||||
height?: string;
|
||||
isEmpty?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
isCardClickable?: boolean;
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ export const Icon = ({
|
||||
size = iconDefaults.size,
|
||||
color = iconDefaults.color,
|
||||
rotate = iconDefaults.rotate,
|
||||
weight,
|
||||
...props
|
||||
}: IconProps) => {
|
||||
const { filled, outlined } = getIconNames();
|
||||
@ -52,6 +53,7 @@ export const Icon = ({
|
||||
color: getColor(color),
|
||||
}}
|
||||
style={{ color: getColor(color) }}
|
||||
weight={source === 'phosphor' ? weight : undefined} // Phosphor icons use 'weight' prop
|
||||
/>
|
||||
</IconWrapper>
|
||||
);
|
||||
|
||||
@ -15,11 +15,13 @@ const names = createEnum(AVAILABLE_ICONS);
|
||||
export type IconNames = keyof typeof names;
|
||||
|
||||
export type MaterialIconVariant = 'filled' | 'outline';
|
||||
export type PhosphorIconWeight = 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone';
|
||||
export type IconSource = 'material' | 'phosphor';
|
||||
|
||||
export interface IconPropsDefaults {
|
||||
source: IconSource;
|
||||
variant: MaterialIconVariant;
|
||||
weight?: PhosphorIconWeight;
|
||||
size: FontSizeOptions;
|
||||
color: FontColorOptions;
|
||||
rotate: RotationOptions;
|
||||
|
||||
@ -87,7 +87,7 @@ export type BorderRadiusOptions = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
|
||||
export type BoxShadowOptions = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'inner' | 'outline' | 'dropdown' | 'none';
|
||||
|
||||
export type SpacingOptions = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
|
||||
export type SpacingOptions = 'none' | 'normal' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
|
||||
|
||||
export type RotationOptions = '0' | '90' | '180' | '270';
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import { HomePage } from '@app/home/HomePage';
|
||||
import { HomePage as HomePageV2 } from '@app/homeV2/HomePage';
|
||||
import { IntroduceYourself } from '@app/homeV2/introduce/IntroduceYourself';
|
||||
import { useSetUserPersona } from '@app/homeV2/persona/useUserPersona';
|
||||
import { HomePage as HomePageNew } from '@app/homepageV2/HomePage';
|
||||
import { HomePage as HomePageV3 } from '@app/homeV3/HomePage';
|
||||
import { useSetUserTitle } from '@app/identity/user/useUserTitle';
|
||||
import { OnboardingContextProvider } from '@app/onboarding/OnboardingContextProvider';
|
||||
import { useAppConfig } from '@app/useAppConfig';
|
||||
@ -41,7 +41,7 @@ export const ProtectedRoutes = (): JSX.Element => {
|
||||
let FinalHomePage;
|
||||
|
||||
if (isThemeV2) {
|
||||
FinalHomePage = showHomepageRedesign ? HomePageNew : HomePageV2;
|
||||
FinalHomePage = showHomepageRedesign ? HomePageV3 : HomePageV2;
|
||||
} else {
|
||||
FinalHomePage = HomePage;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { useBatchGetStepStatesQuery } from '@graphql/step.generated';
|
||||
export const useGetLastViewedAnnouncementTime = () => {
|
||||
const { user } = useUserContext();
|
||||
const finalStepId = `${user?.urn}-${LAST_VIEWED_ANNOUNCEMENT_TIME_STEP}`;
|
||||
const { data, refetch } = useBatchGetStepStatesQuery({
|
||||
const { data, refetch, loading } = useBatchGetStepStatesQuery({
|
||||
skip: !user?.urn,
|
||||
variables: { input: { ids: [finalStepId] } },
|
||||
});
|
||||
@ -16,5 +16,6 @@ export const useGetLastViewedAnnouncementTime = () => {
|
||||
return {
|
||||
time: (lastViewedAnnouncementTimeProperty?.value && Number(lastViewedAnnouncementTimeProperty?.value)) || null,
|
||||
refetch,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
24
datahub-web-react/src/app/homeV3/HomePage.tsx
Normal file
24
datahub-web-react/src/app/homeV3/HomePage.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import PersonalizationLoadingModal from '@app/homeV2/persona/PersonalizationLoadingModal';
|
||||
import HomePageContent from '@app/homeV3/HomePageContent';
|
||||
import Header from '@app/homeV3/header/Header';
|
||||
import { HomePageContainer, PageWrapper, StyledVectorBackground } from '@app/homeV3/styledComponents';
|
||||
import { SearchablePage } from '@app/searchV2/SearchablePage';
|
||||
|
||||
export const HomePage = () => {
|
||||
return (
|
||||
<>
|
||||
<SearchablePage hideSearchBar>
|
||||
<HomePageContainer>
|
||||
<StyledVectorBackground />
|
||||
<PageWrapper>
|
||||
<Header />
|
||||
<HomePageContent />
|
||||
</PageWrapper>
|
||||
</HomePageContainer>
|
||||
</SearchablePage>
|
||||
<PersonalizationLoadingModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
38
datahub-web-react/src/app/homeV3/HomePageContent.tsx
Normal file
38
datahub-web-react/src/app/homeV3/HomePageContent.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Announcements } from '@app/homeV3/announcements/Announcements';
|
||||
import Module from '@app/homeV3/module/Module';
|
||||
import { ModuleProps } from '@app/homeV3/module/types';
|
||||
import { CenteredContainer, ContentContainer, ContentDiv } from '@app/homeV3/styledComponents';
|
||||
|
||||
const SAMPLE_MODULES: ModuleProps[] = [
|
||||
{
|
||||
name: 'Your Assets',
|
||||
description: 'These are assets you are the owner',
|
||||
type: 'yourAssets',
|
||||
visibility: 'personal',
|
||||
},
|
||||
{
|
||||
name: 'Sample large module',
|
||||
description: 'Description of the sample module',
|
||||
type: 'sampleLarge',
|
||||
visibility: 'global',
|
||||
},
|
||||
];
|
||||
|
||||
const HomePageContent = () => {
|
||||
return (
|
||||
<ContentContainer>
|
||||
<CenteredContainer>
|
||||
<ContentDiv>
|
||||
<Announcements />
|
||||
{SAMPLE_MODULES.map((sampleModule) => (
|
||||
<Module {...sampleModule} key={sampleModule.name} />
|
||||
))}
|
||||
</ContentDiv>
|
||||
</CenteredContainer>
|
||||
</ContentContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePageContent;
|
||||
@ -0,0 +1,201 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { useUserContext } from '@app/context/useUserContext';
|
||||
import { useGetLastViewedAnnouncementTime } from '@app/homeV2/shared/useGetLastViewedAnnouncementTime';
|
||||
import { useGetAnnouncementsForUser } from '@app/homeV3/announcements/useGetAnnouncementsForUser';
|
||||
|
||||
import { useListPostsQuery } from '@graphql/post.generated';
|
||||
import { useUpdateUserHomePageSettingsMutation } from '@graphql/user.generated';
|
||||
import { PostContentType, PostType } from '@types';
|
||||
|
||||
vi.mock('@app/context/useUserContext');
|
||||
vi.mock('@app/homeV2/shared/useGetLastViewedAnnouncementTime');
|
||||
vi.mock('@graphql/post.generated');
|
||||
vi.mock('@graphql/user.generated');
|
||||
|
||||
const mockUser = {
|
||||
settings: {
|
||||
homePage: {
|
||||
dismissedAnnouncementUrns: ['urn:1'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockPosts = [
|
||||
{
|
||||
urn: 'urn:2',
|
||||
postType: PostType.HomePageAnnouncement,
|
||||
content: { contentType: PostContentType.Text },
|
||||
},
|
||||
{
|
||||
urn: 'urn:3',
|
||||
postType: PostType.HomePageAnnouncement,
|
||||
content: { contentType: PostContentType.Text },
|
||||
},
|
||||
{
|
||||
urn: 'urn:4',
|
||||
postType: PostType.EntityAnnouncement,
|
||||
content: { contentType: PostContentType.Text },
|
||||
},
|
||||
{
|
||||
urn: 'urn:5',
|
||||
postType: PostType.HomePageAnnouncement,
|
||||
content: { contentType: PostContentType.Link },
|
||||
},
|
||||
];
|
||||
|
||||
describe('useGetAnnouncementsForUser', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return filtered announcements and loading state', () => {
|
||||
(useUserContext as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
user: {
|
||||
settings: {
|
||||
homePage: {
|
||||
dismissedAnnouncementUrns: ['urn:1', '', null, undefined, 'urn:2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
(useGetLastViewedAnnouncementTime as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ time: 123 });
|
||||
(useListPostsQuery as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
listPosts: {
|
||||
posts: mockPosts,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
(useUpdateUserHomePageSettingsMutation as unknown as ReturnType<typeof vi.fn>).mockReturnValue([vi.fn()]);
|
||||
|
||||
const { result } = renderHook(() => useGetAnnouncementsForUser());
|
||||
|
||||
const callArgs = (useListPostsQuery as any).mock.calls[0][0];
|
||||
const urnFilter = callArgs.variables.input.orFilters[0].and.find((f: any) => f.field === 'urn');
|
||||
expect(urnFilter.values).toEqual(['urn:1', 'urn:2']);
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
// Only posts with type HomePageAnnouncement and contentType Text
|
||||
expect(result.current.announcements.map((p) => p.urn)).toEqual(['urn:2', 'urn:3']);
|
||||
});
|
||||
|
||||
it('should call updateUserHomePageSettings on dismiss', async () => {
|
||||
const updateUserHomePageSettings = vi.fn().mockResolvedValue({});
|
||||
const refetch = vi.fn();
|
||||
|
||||
(useUserContext as any).mockReturnValue({ user: mockUser });
|
||||
(useGetLastViewedAnnouncementTime as any).mockReturnValue({ time: 123 });
|
||||
(useListPostsQuery as any).mockReturnValue({
|
||||
data: { listPosts: { posts: mockPosts } },
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch,
|
||||
});
|
||||
(useUpdateUserHomePageSettingsMutation as any).mockReturnValue([updateUserHomePageSettings]);
|
||||
|
||||
const { result } = renderHook(() => useGetAnnouncementsForUser());
|
||||
|
||||
await act(async () => {
|
||||
result.current.onDismissAnnouncement('urn:2');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 2100);
|
||||
});
|
||||
});
|
||||
|
||||
expect(updateUserHomePageSettings).toHaveBeenCalledWith({
|
||||
variables: { input: { newDismissedAnnouncements: ['urn:2'] } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter out announcements that are in newDismissedUrns', async () => {
|
||||
(useUserContext as any).mockReturnValue({
|
||||
user: {
|
||||
settings: {
|
||||
homePage: {
|
||||
dismissedAnnouncementUrns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
(useGetLastViewedAnnouncementTime as any).mockReturnValue({ time: 123 });
|
||||
(useListPostsQuery as any).mockReturnValue({
|
||||
data: { listPosts: { posts: mockPosts } },
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
(useUpdateUserHomePageSettingsMutation as any).mockReturnValue([vi.fn()]);
|
||||
|
||||
const { result } = renderHook(() => useGetAnnouncementsForUser());
|
||||
|
||||
expect(result.current.announcements.map((p) => p.urn)).toEqual(['urn:2', 'urn:3']);
|
||||
|
||||
await act(async () => {
|
||||
result.current.onDismissAnnouncement('urn:2');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.announcements.map((p) => p.urn)).toEqual(['urn:3']);
|
||||
});
|
||||
|
||||
it('should handle when lastViewedAnnouncementsTime is undefined', () => {
|
||||
(useUserContext as any).mockReturnValue({ user: mockUser });
|
||||
(useGetLastViewedAnnouncementTime as any).mockReturnValue({});
|
||||
const useListPostsQueryMock = useListPostsQuery as unknown as ReturnType<typeof vi.fn>;
|
||||
useListPostsQueryMock.mockReturnValue({
|
||||
data: { listPosts: { posts: mockPosts } },
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
(useUpdateUserHomePageSettingsMutation as any).mockReturnValue([vi.fn()]);
|
||||
|
||||
renderHook(() => useGetAnnouncementsForUser());
|
||||
|
||||
const callArgs = useListPostsQueryMock.mock.calls[0][0];
|
||||
const lastModifiedFilter = callArgs.variables.input.orFilters[0].and.find(
|
||||
(f: any) => f.field === 'lastModified',
|
||||
);
|
||||
expect(lastModifiedFilter.values).toEqual(['0']);
|
||||
});
|
||||
|
||||
it('should skip query if user is not present', () => {
|
||||
(useUserContext as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ user: null });
|
||||
(useGetLastViewedAnnouncementTime as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ time: 123 });
|
||||
const useListPostsQueryMock = useListPostsQuery as unknown as ReturnType<typeof vi.fn>;
|
||||
useListPostsQueryMock.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
(useUpdateUserHomePageSettingsMutation as unknown as ReturnType<typeof vi.fn>).mockReturnValue([vi.fn()]);
|
||||
|
||||
renderHook(() => useGetAnnouncementsForUser());
|
||||
expect(useListPostsQueryMock).toHaveBeenCalledWith(expect.objectContaining({ skip: true }));
|
||||
});
|
||||
|
||||
it('should skip query if lastViewedTimeLoading is true', () => {
|
||||
(useUserContext as any).mockReturnValue({ user: mockUser });
|
||||
(useGetLastViewedAnnouncementTime as any).mockReturnValue({ time: null, loading: true });
|
||||
const useListPostsQueryMock = useListPostsQuery as unknown as ReturnType<typeof vi.fn>;
|
||||
useListPostsQueryMock.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
(useUpdateUserHomePageSettingsMutation as any).mockReturnValue([vi.fn()]);
|
||||
|
||||
renderHook(() => useGetAnnouncementsForUser());
|
||||
expect(useListPostsQueryMock).toHaveBeenCalledWith(expect.objectContaining({ skip: true }));
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,73 @@
|
||||
import { Card, Icon, Text, colors } from '@components';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { StyledIcon } from '@app/homeV3/styledComponents';
|
||||
import { Editor } from '@src/alchemy-components/components/Editor/Editor';
|
||||
|
||||
import { Post } from '@types';
|
||||
|
||||
const StyledEditor = styled(Editor)`
|
||||
border: none;
|
||||
|
||||
&&& {
|
||||
.remirror-editor {
|
||||
padding: 0;
|
||||
color: ${colors.violet[500]};
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const cardStyles = {
|
||||
backgroundColor: colors.violet[0],
|
||||
padding: '8px',
|
||||
border: 'none',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
announcement: Post;
|
||||
onDismiss: (urn: string) => void;
|
||||
}
|
||||
|
||||
export const AnnouncementCard = ({ announcement, onDismiss }: Props) => {
|
||||
return (
|
||||
<Card
|
||||
icon={<Icon icon="MegaphoneSimple" source="phosphor" color="violet" weight="fill" size="2xl" />}
|
||||
title={
|
||||
<Text color="violet" weight="semiBold" size="md" lineHeight="normal">
|
||||
{announcement.content.title}
|
||||
</Text>
|
||||
}
|
||||
subTitle={
|
||||
announcement.content.description ? (
|
||||
<StyledEditor content={announcement.content.description} readOnly />
|
||||
) : undefined
|
||||
}
|
||||
button={
|
||||
<StyledIcon
|
||||
icon="X"
|
||||
source="phosphor"
|
||||
color="violet"
|
||||
size="xl"
|
||||
onClick={() => onDismiss(announcement.urn)}
|
||||
/>
|
||||
}
|
||||
width="100%"
|
||||
style={cardStyles}
|
||||
isCardClickable={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AnnouncementCard } from '@app/homeV3/announcements/AnnouncementCard';
|
||||
import { useGetAnnouncementsForUser } from '@app/homeV3/announcements/useGetAnnouncementsForUser';
|
||||
|
||||
const AnnouncementsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
export const Announcements = () => {
|
||||
const { announcements, onDismissAnnouncement } = useGetAnnouncementsForUser();
|
||||
|
||||
const sortedAnnouncements = announcements.sort((a, b) => {
|
||||
return b?.lastModified?.time - a?.lastModified?.time;
|
||||
});
|
||||
|
||||
return (
|
||||
<AnnouncementsContainer>
|
||||
{sortedAnnouncements.map((announcement) => (
|
||||
<AnnouncementCard
|
||||
key={announcement.urn}
|
||||
announcement={announcement}
|
||||
onDismiss={onDismissAnnouncement}
|
||||
/>
|
||||
))}
|
||||
</AnnouncementsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useUserContext } from '@app/context/useUserContext';
|
||||
import { useGetLastViewedAnnouncementTime } from '@app/homeV2/shared/useGetLastViewedAnnouncementTime';
|
||||
|
||||
import { useListPostsQuery } from '@graphql/post.generated';
|
||||
import { useUpdateUserHomePageSettingsMutation } from '@graphql/user.generated';
|
||||
import { FilterOperator, Post, PostContentType, PostType } from '@types';
|
||||
|
||||
export const useGetAnnouncementsForUser = () => {
|
||||
const { user } = useUserContext();
|
||||
const { time: lastViewedAnnouncementsTime, loading: lastViewedTimeLoading } = useGetLastViewedAnnouncementTime();
|
||||
const [updateUserHomePageSettings] = useUpdateUserHomePageSettingsMutation();
|
||||
const [newDismissedUrns, setNewDismissedUrns] = useState<string[]>([]);
|
||||
|
||||
const dismissedUrns = (user?.settings?.homePage?.dismissedAnnouncementUrns || []).filter((urn): urn is string =>
|
||||
Boolean(urn),
|
||||
);
|
||||
|
||||
const getUserPostsFilters = () => [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
field: 'type',
|
||||
condition: FilterOperator.Equal,
|
||||
values: ['HOME_PAGE_ANNOUNCEMENT'],
|
||||
},
|
||||
{
|
||||
field: 'urn',
|
||||
condition: FilterOperator.Equal,
|
||||
values: dismissedUrns,
|
||||
negated: true,
|
||||
},
|
||||
{
|
||||
field: 'lastModified',
|
||||
condition: FilterOperator.GreaterThan,
|
||||
values: [(lastViewedAnnouncementsTime || 0).toString()],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const inputs = {
|
||||
start: 0,
|
||||
count: 30,
|
||||
orFilters: getUserPostsFilters(),
|
||||
};
|
||||
|
||||
const {
|
||||
data: postsData,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
} = useListPostsQuery({
|
||||
variables: {
|
||||
input: inputs,
|
||||
},
|
||||
skip: !user || lastViewedTimeLoading,
|
||||
});
|
||||
|
||||
const announcementsData: Post[] =
|
||||
postsData?.listPosts?.posts
|
||||
.filter((post) => post.postType === PostType.HomePageAnnouncement)
|
||||
.filter((post) => post.content.contentType === PostContentType.Text)
|
||||
.map((post) => post as Post) || [];
|
||||
|
||||
const onDismissAnnouncement = (urn: string) => {
|
||||
setNewDismissedUrns((prev) => [...prev, urn]);
|
||||
|
||||
updateUserHomePageSettings({
|
||||
variables: {
|
||||
input: {
|
||||
newDismissedAnnouncements: [urn],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const announcements = announcementsData.filter((announcement) => !newDismissedUrns.includes(announcement.urn));
|
||||
|
||||
return { announcements, loading, error, refetch, onDismissAnnouncement };
|
||||
};
|
||||
@ -2,25 +2,20 @@ import { colors } from '@components';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import GreetingText from '@app/homepageV2/header/components/GreetingText';
|
||||
import SearchBar from '@app/homepageV2/header/components/SearchBar';
|
||||
import GreetingText from '@app/homeV3/header/components/GreetingText';
|
||||
import SearchBar from '@app/homeV3/header/components/SearchBar';
|
||||
import { CenteredContainer } from '@app/homeV3/styledComponents';
|
||||
|
||||
export const HeaderWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 27px 0 24px 0;
|
||||
padding: 27px 40px 24px 40px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #f8fcff 0%, #fafafb 100%);
|
||||
border: 1px solid ${colors.gray[100]};
|
||||
border-radius: 12px 12px 0 0;
|
||||
`;
|
||||
|
||||
const CenteredContainer = styled.div`
|
||||
max-width: 1016px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<HeaderWrapper>
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import SampleLargeModule from '@app/homepageV2/module/modules/SampleLargeModule';
|
||||
import YourAssetsModule from '@app/homepageV2/module/modules/YourAssetsModule';
|
||||
import { ModuleProps } from '@app/homepageV2/module/types';
|
||||
import { ModuleProps } from '@app/homeV3/module/types';
|
||||
import SampleLargeModule from '@app/homeV3/modules/SampleLargeModule';
|
||||
import YourAssetsModule from '@app/homeV3/modules/YourAssetsModule';
|
||||
|
||||
export default function Module(props: ModuleProps) {
|
||||
const Component = useMemo(() => {
|
||||
@ -2,12 +2,12 @@ import { Loader, borders, colors, radius, spacing } from '@components';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ModuleContainer from '@app/homepageV2/module/components/ModuleContainer';
|
||||
import ModuleDescription from '@app/homepageV2/module/components/ModuleDescription';
|
||||
import ModuleMenu from '@app/homepageV2/module/components/ModuleMenu';
|
||||
import ModuleName from '@app/homepageV2/module/components/ModuleName';
|
||||
import PublicModuleBadge from '@app/homepageV2/module/components/PublicModuleBadge';
|
||||
import { ModuleProps } from '@app/homepageV2/module/types';
|
||||
import ModuleContainer from '@app/homeV3/module/components/ModuleContainer';
|
||||
import ModuleDescription from '@app/homeV3/module/components/ModuleDescription';
|
||||
import ModuleMenu from '@app/homeV3/module/components/ModuleMenu';
|
||||
import ModuleName from '@app/homeV3/module/components/ModuleName';
|
||||
import PublicModuleBadge from '@app/homeV3/module/components/PublicModuleBadge';
|
||||
import { ModuleProps } from '@app/homeV3/module/types';
|
||||
|
||||
const ModuleHeader = styled.div`
|
||||
position: relative;
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import LargeModule from '@app/homepageV2/module/components/LargeModule';
|
||||
import { ModuleProps } from '@app/homepageV2/module/types';
|
||||
import LargeModule from '@app/homeV3/module/components/LargeModule';
|
||||
import { ModuleProps } from '@app/homeV3/module/types';
|
||||
|
||||
export default function SampleLargeModule(props: ModuleProps) {
|
||||
return <LargeModule {...props}>Content of the sample module</LargeModule>;
|
||||
@ -2,10 +2,10 @@ import React from 'react';
|
||||
|
||||
import { useUserContext } from '@app/context/useUserContext';
|
||||
import { useGetAssetsYouOwn } from '@app/homeV2/reference/sections/assets/useGetAssetsYouOwn';
|
||||
import EmptyContent from '@app/homepageV2/module/components/EmptyContent';
|
||||
import EntityItem from '@app/homepageV2/module/components/EntityItem';
|
||||
import LargeModule from '@app/homepageV2/module/components/LargeModule';
|
||||
import { ModuleProps } from '@app/homepageV2/module/types';
|
||||
import EmptyContent from '@app/homeV3/module/components/EmptyContent';
|
||||
import EntityItem from '@app/homeV3/module/components/EntityItem';
|
||||
import LargeModule from '@app/homeV3/module/components/LargeModule';
|
||||
import { ModuleProps } from '@app/homeV3/module/types';
|
||||
|
||||
export default function YourAssetsModule(props: ModuleProps) {
|
||||
const { user } = useUserContext();
|
||||
@ -1,4 +1,4 @@
|
||||
import { colors } from '@components';
|
||||
import { Icon, colors } from '@components';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import VectorBackground from '@images/homepage-vector.svg?react';
|
||||
@ -6,42 +6,55 @@ import VectorBackground from '@images/homepage-vector.svg?react';
|
||||
export const PageWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0px 4px 8px 0px rgba(33, 23, 95, 0.08);
|
||||
`;
|
||||
|
||||
export const HeaderWrapper = styled.div`
|
||||
height: 160px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #f8fcff 0%, #fafafb 100%);
|
||||
border: 1px solid ${colors.gray[100]};
|
||||
border-radius: 12px 12px 0 0;
|
||||
`;
|
||||
|
||||
export const ContentWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const HomePageContainer = styled.div`
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const ContentContainer = styled.div`
|
||||
position: relative; // to enable z-index
|
||||
padding: 40px 160px 16px 160px;
|
||||
background-color: ${colors.white};
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
margin: 5px;
|
||||
`;
|
||||
|
||||
export const StyledVectorBackground = styled(VectorBackground)`
|
||||
width: 100%;
|
||||
transform: rotate(0deg);
|
||||
position: absolute;
|
||||
background: linear-gradient(180deg, #fbfbff 0%, #fafafb 74.57%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
transform: rotate(0deg);
|
||||
pointer-events: none;
|
||||
border-radius: 12px;
|
||||
background-color: ${colors.white};
|
||||
`;
|
||||
|
||||
export const ContentContainer = styled.div`
|
||||
z-index: 1;
|
||||
padding: 40px 40px 16px 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export const CenteredContainer = styled.div`
|
||||
max-width: 1016px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const ContentDiv = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
export const StyledIcon = styled(Icon)`
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import PersonalizationLoadingModal from '@app/homeV2/persona/PersonalizationLoadingModal';
|
||||
import HomePageContent from '@app/homepageV2/HomePageContent';
|
||||
import Header from '@app/homepageV2/header/Header';
|
||||
import { PageWrapper } from '@app/homepageV2/styledComponents';
|
||||
import { SearchablePage } from '@app/searchV2/SearchablePage';
|
||||
|
||||
export const HomePage = () => {
|
||||
return (
|
||||
<>
|
||||
<SearchablePage hideSearchBar>
|
||||
<PageWrapper>
|
||||
<Header />
|
||||
<HomePageContent />
|
||||
</PageWrapper>
|
||||
</SearchablePage>
|
||||
<PersonalizationLoadingModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Module from '@app/homepageV2/module/Module';
|
||||
import { ModuleProps } from '@app/homepageV2/module/types';
|
||||
import { ContentContainer, ContentWrapper, StyledVectorBackground } from '@app/homepageV2/styledComponents';
|
||||
|
||||
const SAMPLE_MODULES: ModuleProps[] = [
|
||||
{
|
||||
name: 'Your Assets',
|
||||
description: 'These are assets you are the owner',
|
||||
type: 'yourAssets',
|
||||
visibility: 'personal',
|
||||
},
|
||||
{
|
||||
name: 'Sample large module',
|
||||
description: 'Description of the sample module',
|
||||
type: 'sampleLarge',
|
||||
visibility: 'global',
|
||||
},
|
||||
];
|
||||
|
||||
const HomePageContent = () => {
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<StyledVectorBackground />
|
||||
<ContentContainer>
|
||||
{SAMPLE_MODULES.map((sampleModule) => (
|
||||
<Module {...sampleModule} key={sampleModule.name} />
|
||||
))}
|
||||
</ContentContainer>
|
||||
</ContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePageContent;
|
||||
@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { HeaderWrapper } from '@app/homepageV2/styledComponents';
|
||||
|
||||
const HomePageHeader = () => {
|
||||
return <HeaderWrapper />;
|
||||
};
|
||||
|
||||
export default HomePageHeader;
|
||||
@ -35,6 +35,12 @@ query getMe {
|
||||
urn
|
||||
}
|
||||
}
|
||||
homePage {
|
||||
pageTemplate {
|
||||
urn
|
||||
}
|
||||
dismissedAnnouncementUrns
|
||||
}
|
||||
}
|
||||
}
|
||||
platformPrivileges {
|
||||
|
||||
@ -268,6 +268,10 @@ mutation updateCorpUserViewsSettings($input: UpdateCorpUserViewsSettingsInput!)
|
||||
updateCorpUserViewsSettings(input: $input)
|
||||
}
|
||||
|
||||
mutation updateUserHomePageSettings($input: UpdateUserHomePageSettingsInput!) {
|
||||
updateUserHomePageSettings(input: $input)
|
||||
}
|
||||
|
||||
query getUserOwnedAssets($urns: [String!]!) {
|
||||
searchAcrossEntities(input: { query: "", filters: [{ field: "owners", values: $urns }] }) {
|
||||
...searchResults
|
||||
|
||||
@ -9,5 +9,10 @@ record CorpUserHomePageSettings {
|
||||
/**
|
||||
* The page template that will be rendered in the UI by default for this user
|
||||
*/
|
||||
pageTemplate: Urn
|
||||
pageTemplate: optional Urn
|
||||
|
||||
/**
|
||||
* The list of announcement urns that have been dismissed by the user
|
||||
*/
|
||||
dismissedAnnouncements: optional array [Urn]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user