Skip to content

Commit

Permalink
feat(ui) Create page for managing home page posts (#8707)
Browse files Browse the repository at this point in the history
  • Loading branch information
chriscollins3456 authored Aug 24, 2023
1 parent a4cb81c commit e12d910
Show file tree
Hide file tree
Showing 21 changed files with 699 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,31 @@ public static boolean canEditGroupMembers(@Nonnull String groupUrnStr, @Nonnull
}

public static boolean canCreateGlobalAnnouncements(@Nonnull QueryContext context) {
return isAuthorized(context, Optional.empty(), PoliciesConfig.CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE);
final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(
ImmutableList.of(
new ConjunctivePrivilegeGroup(ImmutableList.of(
PoliciesConfig.CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE.getType())),
new ConjunctivePrivilegeGroup(ImmutableList.of(
PoliciesConfig.MANAGE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE.getType()))
));

return AuthorizationUtils.isAuthorized(
context.getAuthorizer(),
context.getActorUrn(),
orPrivilegeGroups);
}

public static boolean canManageGlobalAnnouncements(@Nonnull QueryContext context) {
final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(
ImmutableList.of(
new ConjunctivePrivilegeGroup(ImmutableList.of(
PoliciesConfig.MANAGE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE.getType()))
));

return AuthorizationUtils.isAuthorized(
context.getAuthorizer(),
context.getActorUrn(),
orPrivilegeGroups);
}

public static boolean canManageGlobalViews(@Nonnull QueryContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public CompletableFuture<AuthenticatedUser> get(DataFetchingEnvironment environm
platformPrivileges.setManageTags(AuthorizationUtils.canManageTags(context));
platformPrivileges.setManageGlobalViews(AuthorizationUtils.canManageGlobalViews(context));
platformPrivileges.setManageOwnershipTypes(AuthorizationUtils.canManageOwnershipTypes(context));
platformPrivileges.setManageGlobalAnnouncements(AuthorizationUtils.canManageGlobalAnnouncements(context));

// Construct and return authenticated user object.
final AuthenticatedUser authUser = new AuthenticatedUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class DeletePostResolver implements DataFetcher<CompletableFuture<Boolean
public CompletableFuture<Boolean> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();

if (!AuthorizationUtils.canCreateGlobalAnnouncements(context)) {
if (!AuthorizationUtils.canManageGlobalAnnouncements(context)) {
throw new AuthorizationException(
"Unauthorized to delete posts. Please contact your DataHub administrator if this needs corrective action.");
}
Expand Down
5 changes: 5 additions & 0 deletions datahub-graphql-core/src/main/resources/app.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ type PlatformPrivileges {
Whether the user should be able to create, update, and delete ownership types.
"""
manageOwnershipTypes: Boolean!

"""
Whether the user can create and delete posts pinned to the home page.
"""
manageGlobalAnnouncements: Boolean!
}

"""
Expand Down
2 changes: 2 additions & 0 deletions datahub-web-react/src/Mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3363,6 +3363,7 @@ export const mocks = [
generatePersonalAccessTokens: true,
manageGlobalViews: true,
manageOwnershipTypes: true,
manageGlobalAnnouncements: true,
},
},
},
Expand Down Expand Up @@ -3609,4 +3610,5 @@ export const platformPrivileges: PlatformPrivileges = {
createDomains: true,
manageGlobalViews: true,
manageOwnershipTypes: true,
manageGlobalAnnouncements: true,
};
25 changes: 16 additions & 9 deletions datahub-web-react/src/app/search/PostLinkCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,17 @@ const TextContainer = styled.div`
flex: 2;
`;

const TextWrapper = styled.div`
text-align: left;
const FlexWrapper = styled.div<{ alignCenter?: boolean }>`
display: flex;
flex-direction: column;
justify-content: center;
flex: 2;
${(props) => props.alignCenter && 'align-items: center;'}
`;

const TextWrapper = styled.div`
display: flex;
flex-direction: column;
`;

const HeaderText = styled(Typography.Text)`
Expand Down Expand Up @@ -74,19 +79,21 @@ export const PostLinkCard = ({ linkPost }: Props) => {
const link = linkPost?.content?.link || '';

return (
<CardContainer type="link" href={link}>
<CardContainer type="link" href={link} target="_blank" rel="noopener noreferrer">
{hasMedia && (
<LogoContainer>
<PlatformLogo width={50} height={50} preview={false} src={linkPost?.content?.media?.location} />
</LogoContainer>
)}
<TextContainer>
<TextWrapper style={{ textAlign: 'left' }}>
<HeaderText type="secondary">Link</HeaderText>
<Title style={{ margin: 0 }} ellipsis={{ rows: 2 }} level={5}>
{linkPost?.content?.title}
</Title>
</TextWrapper>
<FlexWrapper alignCenter={!hasMedia}>
<TextWrapper>
<HeaderText type="secondary">Link</HeaderText>
<Title style={{ margin: 0 }} ellipsis={{ rows: 2 }} level={5}>
{linkPost?.content?.title}
</Title>
</TextWrapper>
</FlexWrapper>
<StyledArrowOutlined />
</TextContainer>
</CardContainer>
Expand Down
5 changes: 4 additions & 1 deletion datahub-web-react/src/app/search/PostTextCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { Post } from '../../types.generated';
const CardContainer = styled.div`
display: flex;
flex-direction: row;
min-height: 140px;
border: 1px solid ${ANTD_GRAY[4]};
border-radius: 12px;
box-shadow: ${(props) => props.theme.styles['box-shadow']};
&&:hover {
box-shadow: ${(props) => props.theme.styles['box-shadow-hover']};
}
white-space: unset;
padding-bottom: 4px;
`;

const TextContainer = styled.div`
Expand All @@ -28,6 +28,9 @@ const TextContainer = styled.div`
const TitleText = styled(Typography.Title)`
word-break: break-word;
min-height: 20px;
&&& {
margin-top: 8px;
}
`;

const HeaderText = styled(Typography.Text)`
Expand Down
9 changes: 9 additions & 0 deletions datahub-web-react/src/app/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ToolOutlined,
FilterOutlined,
TeamOutlined,
PushpinOutlined,
} from '@ant-design/icons';
import { Redirect, Route, useHistory, useLocation, useRouteMatch, Switch } from 'react-router';
import styled from 'styled-components';
Expand All @@ -19,6 +20,7 @@ import { Preferences } from './Preferences';
import { ManageViews } from '../entity/view/ManageViews';
import { useUserContext } from '../context/useUserContext';
import { ManageOwnership } from '../entity/ownership/ManageOwnership';
import ManagePosts from './posts/ManagePosts';

const PageContainer = styled.div`
display: flex;
Expand Down Expand Up @@ -62,6 +64,7 @@ const PATHS = [
{ path: 'preferences', content: <Preferences /> },
{ path: 'views', content: <ManageViews /> },
{ path: 'ownership', content: <ManageOwnership /> },
{ path: 'posts', content: <ManagePosts /> },
];

/**
Expand Down Expand Up @@ -91,6 +94,7 @@ export const SettingsPage = () => {
const showUsersGroups = (isIdentityManagementEnabled && me && me?.platformPrivileges?.manageIdentities) || false;
const showViews = isViewsEnabled || false;
const showOwnershipTypes = me && me?.platformPrivileges?.manageOwnershipTypes;
const showHomePagePosts = me && me?.platformPrivileges?.manageGlobalAnnouncements;

return (
<PageContainer>
Expand Down Expand Up @@ -143,6 +147,11 @@ export const SettingsPage = () => {
<TeamOutlined /> <ItemTitle>Ownership Types</ItemTitle>
</Menu.Item>
)}
{showHomePagePosts && (
<Menu.Item key="posts">
<PushpinOutlined /> <ItemTitle>Home Page Posts</ItemTitle>
</Menu.Item>
)}
</Menu.ItemGroup>

<Menu.ItemGroup title="Preferences">
Expand Down
91 changes: 91 additions & 0 deletions datahub-web-react/src/app/settings/posts/CreatePostForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { Form, Input, Typography, FormInstance, Radio } from 'antd';
import styled from 'styled-components';
import {
DESCRIPTION_FIELD_NAME,
LINK_FIELD_NAME,
LOCATION_FIELD_NAME,
TITLE_FIELD_NAME,
TYPE_FIELD_NAME,
} from './constants';
import { PostContentType } from '../../../types.generated';

const TopFormItem = styled(Form.Item)`
margin-bottom: 24px;
`;

const SubFormItem = styled(Form.Item)`
margin-bottom: 0;
`;

type Props = {
setCreateButtonEnabled: (isEnabled: boolean) => void;
form: FormInstance;
};

export default function CreatePostForm({ setCreateButtonEnabled, form }: Props) {
const [postType, setPostType] = useState<PostContentType>(PostContentType.Text);

return (
<Form
form={form}
initialValues={{}}
layout="vertical"
onFieldsChange={() => {
setCreateButtonEnabled(!form.getFieldsError().some((field) => field.errors.length > 0));
}}
>
<TopFormItem name={TYPE_FIELD_NAME} label={<Typography.Text strong>Post Type</Typography.Text>}>
<Radio.Group
onChange={(e) => setPostType(e.target.value)}
value={postType}
defaultValue={postType}
optionType="button"
buttonStyle="solid"
>
<Radio value={PostContentType.Text}>Announcement</Radio>
<Radio value={PostContentType.Link}>Link</Radio>
</Radio.Group>
</TopFormItem>

<TopFormItem label={<Typography.Text strong>Title</Typography.Text>}>
<Typography.Paragraph>The title for your new post.</Typography.Paragraph>
<SubFormItem name={TITLE_FIELD_NAME} rules={[{ required: true }]} hasFeedback>
<Input data-testid="create-post-title" placeholder="Your post title" />
</SubFormItem>
</TopFormItem>
{postType === PostContentType.Text && (
<TopFormItem label={<Typography.Text strong>Description</Typography.Text>}>
<Typography.Paragraph>The main content for your new post.</Typography.Paragraph>
<SubFormItem name={DESCRIPTION_FIELD_NAME} rules={[{ min: 0, max: 500 }]} hasFeedback>
<Input.TextArea placeholder="Your post description" />
</SubFormItem>
</TopFormItem>
)}
{postType === PostContentType.Link && (
<>
<TopFormItem label={<Typography.Text strong>Link URL</Typography.Text>}>
<Typography.Paragraph>
Where users will be directed when they click this post.
</Typography.Paragraph>
<SubFormItem name={LINK_FIELD_NAME} rules={[{ type: 'url', warningOnly: true }]} hasFeedback>
<Input data-testid="create-post-link" placeholder="Your post link URL" />
</SubFormItem>
</TopFormItem>
<SubFormItem label={<Typography.Text strong>Image URL</Typography.Text>}>
<Typography.Paragraph>
A URL to an image you want to display on your link post.
</Typography.Paragraph>
<SubFormItem
name={LOCATION_FIELD_NAME}
rules={[{ type: 'url', warningOnly: true }]}
hasFeedback
>
<Input data-testid="create-post-media-location" placeholder="Your post image URL" />
</SubFormItem>
</SubFormItem>
</>
)}
</Form>
);
}
107 changes: 107 additions & 0 deletions datahub-web-react/src/app/settings/posts/CreatePostModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { useState } from 'react';
import { Button, Form, message, Modal } from 'antd';
import CreatePostForm from './CreatePostForm';
import {
CREATE_POST_BUTTON_ID,
DESCRIPTION_FIELD_NAME,
LINK_FIELD_NAME,
LOCATION_FIELD_NAME,
TYPE_FIELD_NAME,
TITLE_FIELD_NAME,
} from './constants';
import { useEnterKeyListener } from '../../shared/useEnterKeyListener';
import { MediaType, PostContentType, PostType } from '../../../types.generated';
import { useCreatePostMutation } from '../../../graphql/mutations.generated';

type Props = {
onClose: () => void;
onCreate: (
contentType: string,
title: string,
description: string | undefined,
link: string | undefined,
location: string | undefined,
) => void;
};

export default function CreatePostModal({ onClose, onCreate }: Props) {
const [createPostMutation] = useCreatePostMutation();
const [createButtonEnabled, setCreateButtonEnabled] = useState(false);
const [form] = Form.useForm();
const onCreatePost = () => {
const contentTypeValue = form.getFieldValue(TYPE_FIELD_NAME) ?? PostContentType.Text;
const mediaValue =
form.getFieldValue(TYPE_FIELD_NAME) && form.getFieldValue(LOCATION_FIELD_NAME)
? {
type: MediaType.Image,
location: form.getFieldValue(LOCATION_FIELD_NAME) ?? null,
}
: null;
createPostMutation({
variables: {
input: {
postType: PostType.HomePageAnnouncement,
content: {
contentType: contentTypeValue,
title: form.getFieldValue(TITLE_FIELD_NAME),
description: form.getFieldValue(DESCRIPTION_FIELD_NAME) ?? null,
link: form.getFieldValue(LINK_FIELD_NAME) ?? null,
media: mediaValue,
},
},
},
})
.then(({ errors }) => {
if (!errors) {
message.success({
content: `Created Post!`,
duration: 3,
});
onCreate(
form.getFieldValue(TYPE_FIELD_NAME) ?? PostContentType.Text,
form.getFieldValue(TITLE_FIELD_NAME),
form.getFieldValue(DESCRIPTION_FIELD_NAME),
form.getFieldValue(LINK_FIELD_NAME),
form.getFieldValue(LOCATION_FIELD_NAME),
);
form.resetFields();
}
})
.catch((e) => {
message.destroy();
message.error({ content: 'Failed to create Post! An unknown error occured.', duration: 3 });
console.error('Failed to create Post:', e.message);
});
onClose();
};

// Handle the Enter press
useEnterKeyListener({
querySelectorToExecuteClick: '#createPostButton',
});

return (
<Modal
title="Create new Post"
open
onCancel={onClose}
footer={
<>
<Button onClick={onClose} type="text">
Cancel
</Button>
<Button
id={CREATE_POST_BUTTON_ID}
data-testid="create-post-button"
onClick={onCreatePost}
disabled={!createButtonEnabled}
>
Create
</Button>
</>
}
>
<CreatePostForm setCreateButtonEnabled={setCreateButtonEnabled} form={form} />
</Modal>
);
}
Loading

0 comments on commit e12d910

Please sign in to comment.