Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EASI-4636] GRB discussions card #2889

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4151469
DiscussionReply component
aterstriep Nov 12, 2024
fecb6f4
DiscussionsCard component
aterstriep Nov 12, 2024
8f52a9e
Mock types and data
aterstriep Nov 13, 2024
5e3aaa5
DiscussionReply component layout
aterstriep Nov 13, 2024
195a41b
Merge remote-tracking branch 'origin/EASI-4635/initial-discussions-ap…
aterstriep Nov 14, 2024
ea25e49
Add discussions to GRB review query
aterstriep Nov 14, 2024
b50a1a7
Update component types
aterstriep Nov 14, 2024
21c028e
Merge branch 'main' into EASI-4636/discussions-response-component
aterstriep Nov 14, 2024
664b217
Translations
aterstriep Nov 14, 2024
2997ce2
Relative date util function
aterstriep Nov 14, 2024
a372467
Merge branch 'feature/EASI-4614_grb_discussions' into EASI-4636/discu…
aterstriep Nov 14, 2024
6ce43e2
Discussions admin card
aterstriep Nov 14, 2024
64b0349
Discussion board tips
aterstriep Nov 15, 2024
90e9a48
Merge branch 'feature/EASI-4614_grb_discussions' into EASI-4636/discu…
aterstriep Nov 15, 2024
67b332b
Rename component
aterstriep Nov 15, 2024
4e0b9d0
DiscussionPost unit test
aterstriep Nov 15, 2024
2a9cd40
Empty discussions state
aterstriep Nov 15, 2024
aeff1dd
Discussions unit tests
aterstriep Nov 15, 2024
92b1226
Replace mock data with query data
aterstriep Nov 15, 2024
ad2b05a
Responsive styling
aterstriep Nov 15, 2024
6004813
Role fallback text with unit test
aterstriep Nov 15, 2024
829b65c
Update reply text and unit tests
aterstriep Nov 15, 2024
a023593
Add Discussions nav link
aterstriep Nov 15, 2024
902b695
Snapshot update
aterstriep Nov 15, 2024
f644a1e
Merge branch 'feature/EASI-4614_grb_discussions' into EASI-4636/grb-d…
aterstriep Nov 18, 2024
8e2bec4
Add `DiscussionBoard` to Discussions component
aterstriep Nov 18, 2024
0b00481
Update DiscussionPost to handle replies
aterstriep Nov 18, 2024
b5c8a77
Unit tests
aterstriep Nov 18, 2024
8740e54
Removed snapshot
aterstriep Nov 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/data/mock/discussions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
SystemIntakeGRBReviewDiscussionFragment,
SystemIntakeGRBReviewerRole,
SystemIntakeGRBReviewerVotingRole
} from 'gql/gen/graphql';

import { systemIntake } from './systemIntake';
import users from './users';

const mockDiscussions = (
systemIntakeID: string = systemIntake.id
): SystemIntakeGRBReviewDiscussionFragment[] => [
{
__typename: 'SystemIntakeGRBReviewDiscussion',
initialPost: {
__typename: 'SystemIntakeGRBReviewDiscussionPost',
id: '882357e4-c0b0-44ef-b749-f71879ad7878',
content:
'<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>',
votingRole: SystemIntakeGRBReviewerVotingRole.VOTING,
grbRole: SystemIntakeGRBReviewerRole.SUBJECT_MATTER_EXPERT,
createdByUserAccount: {
__typename: 'UserAccount',
id: '034fa2b3-00ff-4ec6-857e-75291a59df74',
commonName: users[5].commonName
},
systemIntakeID,
createdAt: '2024-11-12T10:00:00.368862Z'
},
replies: [
{
__typename: 'SystemIntakeGRBReviewDiscussionPost',
id: '4099dbb7-2752-4bf9-a3e1-da225ceb9fae',
content:
'<p>Nisi nobis consectetur voluptatem neque tempore. Ea nihil sed beatae?</p>',
votingRole: SystemIntakeGRBReviewerVotingRole.NON_VOTING,
grbRole: SystemIntakeGRBReviewerRole.CMCS_REP,
createdByUserAccount: {
__typename: 'UserAccount',
id: '909a7888-4d6f-4bbf-9b9d-71ec1e2c3068',
commonName: users[1].commonName
},
systemIntakeID,
createdAt: '2024-11-13T10:00:00.368862Z'
},
{
__typename: 'SystemIntakeGRBReviewDiscussionPost',
id: '47b0081d-de33-4514-b68f-a7e2bbf2610f',
content:
'<p>Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</p>',
votingRole: SystemIntakeGRBReviewerVotingRole.VOTING,
grbRole: SystemIntakeGRBReviewerRole.CO_CHAIR_CIO,
createdByUserAccount: {
__typename: 'UserAccount',
id: '601d52be-7baa-4b45-91cd-88b4a5935c3f',
commonName: users[7].commonName
},
systemIntakeID,
createdAt: '2024-11-13T10:00:00.368862Z'
}
]
}
];

export default mockDiscussions;
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { gql } from '@apollo/client';

import SystemIntakeGRBReviewDiscussion from './SystemIntakeGRBReviewDiscussion';
import SystemIntakeGRBReviewer from './SystemIntakeGRBReviewer';

const GetSystemIntakeGRBReviewers = gql(/* GraphQL */ `
export default gql(/* GraphQL */ `
${SystemIntakeGRBReviewer}
query GetSystemIntakeGRBReviewers($id: UUID!) {
${SystemIntakeGRBReviewDiscussion}
query GetSystemIntakeGRBReview($id: UUID!) {
systemIntake(id: $id) {
id
grbReviewStartedAt
grbReviewers {
...SystemIntakeGRBReviewer
}
grbDiscussions {
...SystemIntakeGRBReviewDiscussion
}
}
}
`);

export default GetSystemIntakeGRBReviewers;
28 changes: 28 additions & 0 deletions src/gql/apolloGQL/grbReview/SystemIntakeGRBReviewDiscussion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { gql } from '@apollo/client';

const SystemIntakeGRBReviewDiscussionPost = gql(/* GraphQL */ `
fragment SystemIntakeGRBReviewDiscussionPost on SystemIntakeGRBReviewDiscussionPost {
id
content
votingRole
grbRole
createdByUserAccount {
id
commonName
}
systemIntakeID
createdAt
}
`);

export default gql(/* GraphQL */ `
${SystemIntakeGRBReviewDiscussionPost}
fragment SystemIntakeGRBReviewDiscussion on SystemIntakeGRBReviewDiscussion {
initialPost {
...SystemIntakeGRBReviewDiscussionPost
}
replies {
...SystemIntakeGRBReviewDiscussionPost
}
}
`);
74 changes: 54 additions & 20 deletions src/gql/gen/graphql.ts

Large diffs are not rendered by default.

22 changes: 12 additions & 10 deletions src/i18n/en-US/discussions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ const discussions = {
discussedTopics_plural: '{{count}} discussions with replies',
fieldsMarkedRequired:
'Fields marked with an asterisk (<asterisk />) are required.', // TODO: this is in other i18n files, move to general.ts?
nonDiscussedTopics: '{{count}} discussion without replies',
nonDiscussedTopics_plural: '{{count}} discussions without replies',
discussionsWithoutReplies: '{{count}} discussion without replies',
discussionsWithoutReplies_plural: '{{count}} discussions without replies',
readMore: 'Read more', // TODO: this is in other i18n files, move to general.ts?
repliesInDiscussion: '{{count}} reply in this discussion',
repliesInDiscussion_plural: '{{count}} replies in this discussion',
repliesCount: '{{count}} reply',
repliesCount_plural: '{{count}} replies',
reply: 'Reply',
lastReply: 'Last reply {{date}} at {{time}}',
saveDiscussion: 'Save discussion',

startDiscussion: 'Start a new discussion',
Expand All @@ -30,6 +33,8 @@ const discussions = {
alerts: {
noDiscussions:
'There are no discussions yet. When a discussion topic is started, it will appear here.',
noDiscussionsStartButton:
'There are not yet any discussions. <button>Start a discussion</button>.',
replyError:
'There was an issue with adding your reply, please try again.',
replySuccess: 'Success! Your reply has been added.',
Expand All @@ -56,23 +61,20 @@ const discussions = {
label: 'Tips for using the discussion boards',
content: [
'Start a new discussion thread for each new topic',
// 'Use tags (@) any time you need input from a specific individual or group. Group tags will notify all members of that group. Avalailble group tags: @Governance Review Board, @Governance Admin Team, @Admin Lead',
'Use tags (@) any time you need input from a specific individual or group. Group tags will notify all members of that group. Avalailble group tags {{groupNames}}', // TODO: wont display @ symbol?
'Use tags (@) any time you need input from a specific individual or group. Group tags will notify all members of that group. Avalailble group tags: <span>@Governance Review Board</span>, <span>@Governance Admin Team</span>, and <span>@Admin Lead</span>',
'Participating individuals will get an email notification when a new discussion is started, or when they are tagged in a discussion or reply'
]
}
},

// Board Specific Translations
governanceReviewBoard: {
adminPanel: {
// description:
// 'Use the discussion boards below to discuss this project. The internal GRB discussion board is a space for the Governance Admin Team and GRB members to discuss privately; the project team will not be able to view discussions there.',
description:
'Use the discussion boards below to discuss this project. The {{discussionBoardType}} is a space for the {{groupNames}} members to discuss privately; the project team will not be able to view discussions there.'
},
discussionsDescription:
'Use the discussion boards below to discuss this project. The internal GRB discussion board is a space for the Governance Admin Team and GRB members to discuss privately; the project team will not be able to view discussions there.',
governanceAdminTeam: 'Governance Admin Team',
internal: {
label: 'Internal GRB discussion board', // TODO: enum translation?
visibilityRestricted: 'Visibility restricted',
// description:
// 'Use this discussion board to ask questions or have dicussions with the Governance Admin Team and other Governance Review Board (GRB) members. The conversations here are not visible to the Project team.'
description:
Expand Down
31 changes: 31 additions & 0 deletions src/utils/date.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
formatDateLocal,
formatDateUtc,
getFiscalYear,
getRelativeDate,
parseAsUTC
} from './date';

Expand Down Expand Up @@ -91,3 +92,33 @@ describe('getFiscalYear', () => {
expect(getFiscalYear(date)).toEqual(2029);
});
});

describe('getRelativeDate', () => {
it('returns formatted date after 30 days', () => {
const date = DateTime.fromObject({ year: 2021, month: 3, day: 1 });

const formattedDate = date.toFormat('MM/dd/yyyy');

const relativeDate = getRelativeDate(date.toISO());

expect(relativeDate).toEqual(formattedDate);
});

it('formats past relative date', () => {
const days = 3;

const date = DateTime.now().minus({ days });

const relativeDate = getRelativeDate(date.toISO());

expect(relativeDate).toEqual(`${days} days ago`);
});

it('formats relative date for today', () => {
const date = DateTime.now();

const relativeDate = getRelativeDate(date.toISO());

expect(relativeDate).toEqual('today');
});
});
36 changes: 35 additions & 1 deletion src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DateTime } from 'luxon';
import { DateTime, Interval } from 'luxon';

// Used to parse out mintute, day, ,month, and years from ISOString
export const parseAsUTC = (date: string) => DateTime.fromISO(date).toUTC();
Expand Down Expand Up @@ -72,3 +72,37 @@ export const isDateInPast = (date: string | null): boolean => {
}
return false;
};

/**
* If less than 30 days have passed since `date`, returns "today" or "X days ago".
*
* Otherwise, returns formatted date.
*/
export const getRelativeDate = (
date: string | null,
/**
* Number of days between `date` and now to display relative date
* before switching to formatted date
*/
relativeDateLimit: number = 30
): string => {
if (!date) return '';

const dateTime = DateTime.fromISO(date);

if (!dateTime.isValid) return '';

/** Interval between now and `date` */
const interval = Interval.fromDateTimes(dateTime, DateTime.now());

// Subtract one from the interval count to see how many days since the initial date
const days = interval.count('days') - 1;

// If more than 30 days have passed, return formatted date
if (days > relativeDateLimit) {
return DateTime.fromISO(date).toFormat('MM/dd/yyyy');
}

// Return relative date
return dateTime.toRelativeCalendar({ unit: 'days' });
};
9 changes: 9 additions & 0 deletions src/views/DiscussionBoard/DiscussionPost/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.easi-discussion-post {
&__header {
justify-content: space-between;
}

&__content * {
line-height: 1.6 !important;
}
}
85 changes: 85 additions & 0 deletions src/views/DiscussionBoard/DiscussionPost/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { SystemIntakeGRBReviewDiscussionFragment } from 'gql/gen/graphql';
import i18next from 'i18next';

import mockDiscussions from 'data/mock/discussions';
import { getRelativeDate } from 'utils/date';

import DiscussionPost from '.';

const [discussion] = mockDiscussions();
const { initialPost, replies } = discussion;

describe('DiscussionPost', () => {
it('renders a discussion post with replies', () => {
render(
<DiscussionPost
{...discussion.initialPost}
replies={discussion.replies}
/>
);

const {
createdByUserAccount: { commonName },
grbRole,
votingRole,
createdAt
} = initialPost;

expect(screen.getByText(commonName)).toBeInTheDocument();

const formattedRole = `${i18next.t(`grbReview:votingRoles.${votingRole}`)}, ${i18next.t(`grbReview:reviewerRoles.${grbRole}`)}`;
expect(
screen.getByRole('heading', { level: 5, name: formattedRole })
).toBeInTheDocument();

const dateText = getRelativeDate(createdAt);
expect(screen.getByText(dateText)).toBeInTheDocument();

const repliesCount = replies.length;
expect(
screen.getByRole('button', { name: `${repliesCount} replies` })
).toBeInTheDocument();

const lastReplyAtText = i18next.t('discussions:general.lastReply', {
date: getRelativeDate(replies[0].createdAt, 1),
time: '10:00 AM'
});
expect(screen.getByText(lastReplyAtText)).toBeInTheDocument();
});

it('renders a discussion post without replies', () => {
render(<DiscussionPost {...discussion.initialPost} replies={[]} />);

expect(screen.getByRole('button', { name: 'Reply' })).toBeInTheDocument();

expect(screen.queryByTestId('lastReplyAtText')).toBeNull();
});

it('hides discussion reply data', () => {
render(<DiscussionPost {...discussion.initialPost} />);

expect(screen.queryByTestId('discussionReplies')).toBeNull();
});

it('displays roles fallback text', () => {
const discussionNoRole: SystemIntakeGRBReviewDiscussionFragment = {
...discussion,
initialPost: {
...initialPost,
grbRole: null,
votingRole: null
}
};

render(
<DiscussionPost
{...discussionNoRole.initialPost}
replies={discussionNoRole.replies}
/>
);

expect(screen.getByText('Governance Admin Team')).toBeInTheDocument();
});
});
Loading
Loading