strapi-plugin-comments 2.0.0-beta.7 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/README.md +186 -134
  3. package/__mocks__/initSetup.js +53 -0
  4. package/admin/src/components/ActionButton/styles.js +10 -0
  5. package/admin/src/components/DiscussionThread/index.js +3 -7
  6. package/admin/src/components/DiscussionThreadItem/index.js +25 -70
  7. package/admin/src/components/DiscussionThreadItem/styles.js +4 -79
  8. package/admin/src/components/DiscussionThreadItemActions/index.js +82 -40
  9. package/admin/src/components/DiscussionThreadItemActions/styles.js +1 -12
  10. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +2 -3
  11. package/admin/src/components/DiscussionThreadItemFooter/index.js +47 -0
  12. package/admin/src/components/DiscussionThreadItemFooter/styles.js +13 -0
  13. package/admin/src/components/DiscussionThreadItemReviewAction/index.js +131 -0
  14. package/admin/src/components/IconButton/styles.js +14 -0
  15. package/admin/src/components/ReportsReviewModal/index.js +1 -1
  16. package/admin/src/components/ReportsReviewTable/index.js +12 -6
  17. package/admin/src/components/StatusBadge/index.js +17 -0
  18. package/admin/src/components/StatusBadge/styles.js +17 -0
  19. package/admin/src/pages/Details/components/DetailsEntity/index.js +42 -24
  20. package/admin/src/pages/Details/components/DetailsFilters/index.js +8 -10
  21. package/admin/src/pages/Details/index.js +6 -15
  22. package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +30 -67
  23. package/admin/src/translations/en.json +13 -15
  24. package/admin/src/utils/resolveCommentStatus.js +4 -4
  25. package/content-types/comment.js +0 -4
  26. package/package.json +8 -5
  27. package/public/assets/logo.png +0 -0
  28. package/public/assets/preview.png +0 -0
  29. package/server/controllers/client.js +7 -2
  30. package/server/controllers/utils/__tests__/functions.test.js +2 -2
  31. package/server/controllers/utils/parsers.js +3 -1
  32. package/server/graphql/mutations/createAbuseReport.js +8 -3
  33. package/server/graphql/mutations/createComment.js +8 -3
  34. package/server/graphql/mutations/removeComment.js +8 -3
  35. package/server/graphql/mutations/updateComment.js +8 -3
  36. package/server/graphql/resolvers-config.js +13 -8
  37. package/server/graphql/types/comment-nested.js +1 -1
  38. package/server/graphql/types/comment.js +0 -1
  39. package/server/graphql/types/create-comment-author.js +1 -1
  40. package/server/graphql/types/identify-comment-author.js +1 -1
  41. package/server/graphql/types/index.js +0 -1
  42. package/server/services/__tests__/common.test.js +165 -0
  43. package/server/services/admin.js +10 -7
  44. package/server/services/client.js +45 -31
  45. package/server/services/common.js +38 -16
  46. package/server/services/utils/__tests__/functions.test.js +9 -73
  47. package/server/services/utils/functions.js +23 -34
  48. package/server/utils/__tests__/functions.test.js +17 -0
  49. package/admin/src/components/DiscussionThreadItemMeta/index.js +0 -42
  50. package/admin/src/components/DiscussionThreadItemWarnings/index.js +0 -147
  51. package/admin/src/components/DiscussionThreadItemWarnings/styles.js +0 -7
  52. package/server/graphql/types/comment-author-user.js +0 -9
@@ -0,0 +1,53 @@
1
+ const { get, set } = require('lodash');
2
+
3
+ module.exports = (config = {}) => {
4
+ const mock = {
5
+ getRef: function() {
6
+ return this;
7
+ },
8
+ plugin: function(name) {
9
+ return this.plugins[name];
10
+ },
11
+ plugins: {
12
+ comments: {
13
+ service: function(name) {
14
+ return this.services[name]({ strapi: mock.getRef() });
15
+ },
16
+ package: require('../package.json'),
17
+ services: {
18
+ common: require('../server/services/common'),
19
+ client: require('../server/services/client'),
20
+ admin: require('../server/services/admin'),
21
+ },
22
+ contentTypes: {
23
+ comment: {
24
+ ...require('../content-types/comment'),
25
+ uid: 'plugins::comments.comment',
26
+ },
27
+ 'comment-report': {
28
+ ...require('../content-types/report'),
29
+ uid: 'plugins::comments.comment-report'
30
+ },
31
+ }
32
+ }
33
+ },
34
+ config: {
35
+ get: function(prop = '') {
36
+ return get(this.plugins, prop.replace('plugin.', ''));
37
+ },
38
+ set: function(prop = '', value) {
39
+ return set(this.plugins, prop.replace('plugin.', ''), value);
40
+ },
41
+ plugins: {
42
+ comments: {
43
+ approvalFlow: ['api::blog-post.blog-post'],
44
+ entryLabel: {
45
+ 'api::blog-post.blog-post': ['alternative_subject'],
46
+ },
47
+ ...config
48
+ }
49
+ }
50
+ }
51
+ };
52
+ return mock;
53
+ };
@@ -0,0 +1,10 @@
1
+
2
+ import styled from 'styled-components';
3
+ import { Button } from '@strapi/design-system/Button';
4
+
5
+ export const ActionButton = styled(Button)`
6
+ padding: 7px 16px;
7
+ margin-left: ${({ isSingle }) => isSingle ? '0' : '.5rem'};
8
+
9
+ white-space: nowrap;
10
+ `;
@@ -8,7 +8,6 @@ import React from 'react';
8
8
  import PropTypes from 'prop-types';
9
9
  import { isNil } from 'lodash';
10
10
  import { Box } from '@strapi/design-system/Box';
11
- import { Divider } from '@strapi/design-system/Divider';
12
11
  import { Flex } from '@strapi/design-system/Flex';
13
12
  import { Link } from '@strapi/design-system/Link';
14
13
  import { Typography } from '@strapi/design-system/Typography';
@@ -22,22 +21,19 @@ const DiscussionThread = ({ level = [], selected = {}, allowedActions, isReloadi
22
21
  const rootThread = selected?.threadOf;
23
22
  return (<LoadingIndicatorContainer as={Box} padding={4}>
24
23
  { isReloading && <LoadingIndicatorOverlay />}
25
- <Flex as={Box} direction="row" justifyContent="space-between">
26
- <Typography variant="sigma" textColor="neutral600" id="entity-details">
24
+ <Flex as={Box} direction="row" justifyContent="space-between" marginBottom={2}>
25
+ <Typography variant="delta" textColor="neutral800" id="discussion-thread">
27
26
  { getMessage('page.details.panel.discussion', 'Discussion') }
28
27
  </Typography>
29
28
  { rootThread && (<Link to={getUrl(`discover/${rootThread.id}`)} startIcon={<ArrowUp />}>
30
29
  { getMessage('page.details.panel.discussion.nav.back') }
31
30
  </Link>) }
32
31
  </Flex>
33
- <Box paddingTop={2} paddingBottom={6}>
34
- <Divider />
35
- </Box>
36
32
  <Flex as="ul" direction="column" alignItems="flex-start">
37
33
  { rootThread && (<DiscussionThreadItem {...rootThread} allowedActions={allowedActions} isThreadAuthor root pinned />) }
38
34
  { level.map(item => {
39
35
  const isSelected = selected.id === item.id;
40
- const isThreadAuthor = !isNil(selected?.threadOf?.authorId) && (selected?.threadOf?.authorId === item.authorId);
36
+ const isThreadAuthor = !isNil(selected?.threadOf?.author?.id) && (selected?.threadOf?.author?.id === item?.author?.id);
41
37
  return (<DiscussionThreadItem
42
38
  key={`comment-${item.id}`}
43
39
  {...item}
@@ -1,92 +1,47 @@
1
1
  /**
2
2
  *
3
- * Entity Details
3
+ * Discussion thread - Item
4
4
  *
5
5
  */
6
6
 
7
7
  import React from 'react';
8
8
  import PropTypes from 'prop-types';
9
- import { Avatar, Initials } from '@strapi/design-system/Avatar';
10
9
  import { Box } from '@strapi/design-system/Box';
11
- import { Divider } from '@strapi/design-system/Divider';
12
10
  import { Flex } from '@strapi/design-system/Flex';
13
- import { Link } from '@strapi/design-system/Link';
14
11
  import { Typography } from '@strapi/design-system/Typography';
15
- import { ArrowRight } from '@strapi/icons';
16
- import DiscussionThreadItemMeta from '../DiscussionThreadItemMeta';
17
- import { AvatarWrapper, DiscussionThreadItemBox, DiscussionThreadItemBoxContent, DiscussionThreadItemContent, DiscussionThreadItemLeft, DiscussionThreadItemLinkBox, DiscussionThreadItemRight, DiscussionThreadItemRoot } from './styles';
12
+ import DiscussionThreadItemFooter from '../DiscussionThreadItemFooter';
18
13
  import DiscussionThreadItemActions from '../DiscussionThreadItemActions';
19
- import { getMessage, getUrl, renderInitials } from '../../utils';
20
- import DiscussionThreadItemWarnings from '../DiscussionThreadItemWarnings';
14
+ import { DiscussionThreadItemContainer, DiscussionThreadFullsize, DiscussionThreadItemContent } from './styles';
21
15
 
22
16
  const DiscussionThreadItem = (props) => {
23
17
  const {
18
+ as = "li",
24
19
  isSelected,
25
- isThreadAuthor,
26
20
  content,
27
21
  root,
28
22
  preview,
29
- pinned,
30
- blocked,
31
- removed,
32
- blockedThread,
33
- gotThread,
34
- threadFirstItemId,
35
- authorUser,
36
- author } = props;
37
-
38
- let background = 'neutral100';
39
- let borderColor = 'neutral200';
40
- let dividerColor = 'neutral200';
41
- let StyledComponent = DiscussionThreadItemRight;
42
-
43
- if(isThreadAuthor || root){
44
- StyledComponent = DiscussionThreadItemLeft;
45
- borderColor = 'neutral100';
46
- dividerColor = 'neutral100';
47
- background = null;
48
- }
49
-
50
- if (isSelected) {
51
- background = 'secondary100'
52
- borderColor = 'secondary500';
53
- dividerColor = 'secondary200';
54
- }
55
-
56
- if (root) {
57
- StyledComponent = DiscussionThreadItemRoot;
58
- }
59
-
60
- const avatar = author?.avatar || authorUser?.avatar;
61
- const name = author?.name || `${authorUser?.firstName} ${authorUser?.lastName}`;
62
- const isBlocked = blocked || blockedThread;
23
+ pinned } = props;
63
24
 
64
- return (<StyledComponent as="li">
65
- <DiscussionThreadItemContent as={Flex} direction="row">
66
- <AvatarWrapper>
67
- { avatar ? <Avatar src={avatar} alt={name} /> : <Initials>{ renderInitials(name) }</Initials> }
68
- </AvatarWrapper>
69
- <DiscussionThreadItemBox hasRadius shadow="filterShadow" borderColor={borderColor} background={background} padding={2}>
70
- <Flex as={Box} paddingBottom={2} direction="column">
71
- <DiscussionThreadItemBoxContent as={Flex} direction="row">
72
- <Box as={Flex} grow={1} alignItems="center">
73
- <Typography variant="omega">{ content }</Typography>
74
- </Box>
75
- { !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} />) }
76
- </DiscussionThreadItemBoxContent>
77
- { !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...props } gotThread={gotThread || root} />) }
78
- </Flex>
79
- <Divider background={dividerColor} />
80
- <DiscussionThreadItemMeta { ...props } />
81
- </DiscussionThreadItemBox>
82
- </DiscussionThreadItemContent>
83
-
84
- { (gotThread && !(removed || preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
85
- <Link to={getUrl(`discover/${threadFirstItemId}`)} endIcon={<ArrowRight />}>
86
- { getMessage('page.details.panel.discussion.nav.drilldown') }
87
- </Link>
88
- </DiscussionThreadItemLinkBox>) }
89
- </StyledComponent>);
25
+ return (<DiscussionThreadFullsize as={as} marginBottom={preview ? 4 : 0}>
26
+ <DiscussionThreadItemContainer
27
+ as={Flex}
28
+ direction="column"
29
+ alignItems="flex-start"
30
+ hasRadius
31
+ background={isSelected ? 'neutral100' : null}
32
+ paddingLeft={2} paddingRight={2} paddingTop={4} paddingBottom={4}>
33
+ <DiscussionThreadItemContent
34
+ as={Flex}
35
+ paddingBottom={2}
36
+ direction="row">
37
+ <Box as={Flex} grow={1} alignItems="center" marginTop="6px">
38
+ <Typography variant="omega" textColor="neutral800">{ content }</Typography>
39
+ </Box>
40
+ { !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} preview={preview} />) }
41
+ </DiscussionThreadItemContent>
42
+ <DiscussionThreadItemFooter { ...props } />
43
+ </DiscussionThreadItemContainer>
44
+ </DiscussionThreadFullsize>);
90
45
  };
91
46
 
92
47
  DiscussionThreadItem.propTypes = {
@@ -1,88 +1,13 @@
1
1
  import styled from 'styled-components';
2
2
  import { Box } from '@strapi/design-system/Box';
3
- import { Flex } from '@strapi/design-system/Flex';
4
3
 
5
- export const AvatarWrapper = styled(Box)``;
6
- export const InitialsWrapper = styled(Box)``;
7
-
8
- export const DiscussionThreadItemLinkBox = styled(Flex)`
4
+ export const DiscussionThreadFullsize = styled(Box)`
9
5
  width: 100%;
10
- margin-top: .5rem;
11
- margin-bottom: .5rem;
12
6
  `
13
-
14
- const DiscussionThreadItemStyled = styled(Box)`
15
- display: flex;
16
- flex-direction: column;
17
- width: 100%;
18
- margin-bottom: 1rem;
19
-
20
- ${AvatarWrapper},
21
- ${InitialsWrapper} {
22
- align-self: flex-start;
23
- margin-top: .4rem;
24
- }
25
- `;
26
-
27
- export const DiscussionThreadItemContent = styled(Box)`
28
- max-width: 45%;
29
- `;
30
-
31
- export const DiscussionThreadItemLeft = styled(DiscussionThreadItemStyled)`
7
+ export const DiscussionThreadItemContent = styled(DiscussionThreadFullsize)`
32
8
  align-items: flex-start;
33
- padding-left: 4rem;
34
- text-align: left;
35
-
36
- ${DiscussionThreadItemContent} {
37
- align-self: flex-start;
38
- }
39
-
40
- ${AvatarWrapper},
41
- ${InitialsWrapper} {
42
- margin-right: 1rem;
43
- }
44
9
  `;
45
10
 
46
- export const DiscussionThreadItemRight = styled(DiscussionThreadItemStyled)`
47
- align-items: flex-end;
48
- padding-right: 4rem;
49
- text-align: left;
50
-
51
- ${DiscussionThreadItemContent} {
52
- align-self: flex-end;
53
- flex-direction: row-reverse;
54
- }
55
-
56
- ${AvatarWrapper},
57
- ${InitialsWrapper} {
58
- margin-left: 1rem;
59
- }
60
-
61
- ${DiscussionThreadItemLinkBox} {
62
- margin-right: 3rem;
63
- }
64
- `;
65
-
66
- export const DiscussionThreadItemBox = styled(Box)`
67
-
11
+ export const DiscussionThreadItemContainer = styled(DiscussionThreadFullsize)`
12
+ align-items: flex-start;
68
13
  `;
69
-
70
- export const DiscussionThreadItemBoxContent = styled(Box)`
71
- width: 100%;
72
- align-items: stretch;
73
- `
74
-
75
- export const DiscussionThreadItemRoot = styled(DiscussionThreadItemLeft)`
76
- padding-left: 0;
77
- padding-right: 0;
78
-
79
-
80
- ${DiscussionThreadItemContent} {
81
- width: 100%;
82
- max-width: 100%;
83
- }
84
-
85
- ${DiscussionThreadItemBox} {
86
- flex-grow: 1;
87
- }
88
- `;
@@ -7,24 +7,42 @@
7
7
  import React, { useState } from 'react';
8
8
  import PropTypes from 'prop-types';
9
9
  import { useMutation, useQueryClient } from 'react-query';
10
+ import { useHistory } from 'react-router-dom';
10
11
  import { isNil, isEmpty } from 'lodash';
11
- import { Button } from '@strapi/design-system/Button';
12
12
  import { Flex } from '@strapi/design-system/Flex';
13
13
  import { IconButton } from '@strapi/design-system/IconButton';
14
14
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
15
- import { getMessage, handleAPIError, resolveCommentStatus, resolveCommentStatusColor } from '../../utils';
16
- import { DiscussionThreadItemActionsBadge, DiscussionThreadItemActionsWrapper } from './styles';
15
+ import { Eye } from '@strapi/icons';
16
+ import { getMessage, getUrl, handleAPIError, resolveCommentStatus, resolveCommentStatusColor } from '../../utils';
17
+ import { DiscussionThreadItemActionsWrapper } from './styles';
17
18
  import ConfirmationDialog from '../ConfirmationDialog';
18
19
  import { blockItem, blockItemThread, unblockItem, unblockItemThread } from '../../pages/utils/api';
19
20
  import pluginId from '../../pluginId';
20
21
  import { LockIcon, UnlockIcon } from '../icons';
21
22
  import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
22
-
23
- const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, reports = [], approvalStatus, root, allowedActions: { canModerate, canAccessReports, canReviewReports } }) => {
23
+ import StatusBadge from '../StatusBadge';
24
+ import { IconButtonGroupStyled } from '../IconButton/styles';
25
+ import { ActionButton } from '../ActionButton/styles';
26
+ import DiscussionThreadItemReviewAction from '../DiscussionThreadItemReviewAction';
27
+
28
+ const DiscussionThreadItemActions = ({ allowedActions: { canModerate, canAccessReports, canReviewReports }, ...item }) => {
29
+ const { id,
30
+ blocked,
31
+ removed,
32
+ blockedThread,
33
+ gotThread,
34
+ threadItemsCount,
35
+ threadFirstItemId,
36
+ pinned,
37
+ preview,
38
+ reports = [],
39
+ approvalStatus } = item;
24
40
 
25
41
  const [blockConfirmationVisible, setBlockConfirmationVisible] = useState(false);
26
42
  const [blockThreadConfirmationVisible, setBlockThreadConfirmationVisible] = useState(false);
43
+ const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(blockedThread);
27
44
 
45
+ const { push } = useHistory();
28
46
  const toggleNotification = useNotification();
29
47
  const queryClient = useQueryClient();
30
48
  const { lockApp, unlockApp } = useOverlayBlocker();
@@ -75,17 +93,20 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
75
93
  const openReports = reports?.filter(_ => !_.resolved);
76
94
  const hasReports = !isEmpty(openReports);
77
95
  const reviewFlowEnabled = (canAccessReports || canReviewReports) && hasReports;
78
-
79
- const resolveStatusBadge = () => {
80
- const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus, reviewFlowEnabled: !isBlocked && reviewFlowEnabled });
81
- return {
82
- badgeVisible: isBlocked || gotApprovalFlow || removed,
83
- badgeColor: resolveCommentStatusColor(status),
84
- badgeLabel: status,
85
- };
86
- };
87
-
88
- const {badgeVisible, badgeColor, badgeLabel } = resolveStatusBadge();
96
+ const hasActiveThread = gotThread && !(removed || preview || pinned || blockedThread);
97
+ const isStatusBadgeVisible = isBlocked || reviewFlowEnabled;
98
+
99
+ const renderStatus = (props) => {
100
+ const status = resolveCommentStatus({ ...props, reviewFlowEnabled });
101
+ const color = resolveCommentStatusColor(status);
102
+
103
+ return (<StatusBadge backgroundColor={`${color}100`} textColor={`${color}700`} color={color}>{ getMessage({
104
+ id: `page.common.item.status.${status}`,
105
+ props: {
106
+ count: openReports.length
107
+ }
108
+ }, status) }</StatusBadge>);
109
+ };
89
110
 
90
111
  const handleBlockClick = () => setBlockConfirmationVisible(true);
91
112
  const handleBlockConfirm = async () => {
@@ -121,36 +142,57 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
121
142
  }
122
143
  };
123
144
 
145
+ const handleDrilldownClick = () => {
146
+ if (hasActiveThread) {
147
+ push(getUrl(`discover/${threadFirstItemId}`));
148
+ }
149
+ };
150
+
151
+ const handleBlockActionClick = async (mutation, onCallback) => {
152
+ lockApp();
153
+ await mutation.mutateAsync(id);
154
+ onCallback();
155
+ };
156
+
157
+ const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
158
+
159
+ const anyGroupButtonsVisible = needsApproval || reviewFlowEnabled || !blockedThread;
160
+ const isLoading = unblockItemMutation.isLoading || blockItemMutation.isLoading || blockItemThreadMutation.isLoading || unblockItemThreadMutation.isLoading;
161
+
124
162
  if (removed || isRejected || !canModerate) {
125
163
  return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
126
- <DiscussionThreadItemActionsBadge
127
- color={badgeColor}
128
- backgroundColor={`${badgeColor}100`}
129
- textColor={`${badgeColor}600`}>
130
- { getMessage({
131
- id: `page.common.item.status.${badgeLabel}`,
132
- props: { count: openReports.length },
133
- }, badgeLabel)}
134
- </DiscussionThreadItemActionsBadge>
164
+ { renderStatus(item) }
135
165
  </DiscussionThreadItemActionsWrapper>);
136
166
  }
137
167
 
138
168
  return (<>
139
169
  <DiscussionThreadItemActionsWrapper as={Flex} direction="row">
140
- { badgeVisible && (<DiscussionThreadItemActionsBadge
141
- color={badgeColor}
142
- backgroundColor={`${badgeColor}100`}
143
- textColor={`${badgeColor}600`}>
144
- { getMessage({
145
- id: `page.common.item.status.${badgeLabel}`,
146
- props: { count: openReports.length },
147
- }, badgeLabel)}
148
- </DiscussionThreadItemActionsBadge>)}
149
- { !blockedThread && !(blocked || needsApproval) && (<IconButton onClick={handleBlockClick} loading={blockItemMutation.isLoading} icon={<LockIcon />} label={getMessage('page.details.actions.comment.block', 'Block')} style={(!blockedThread && root) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />)}
150
- { !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} style={(blocked && !blockedThread) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />)}
151
- { (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
152
- { (blockedThread && (gotThread || pinned)) && (<Button onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
153
- { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
170
+ { isStatusBadgeVisible && (renderStatus(item))}
171
+ { (!blockedThread && (gotThread || pinned)) && (<ActionButton onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</ActionButton>) }
172
+ { (blockedThread && (gotThread || pinned)) && (<ActionButton onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</ActionButton>) }
173
+ { anyGroupButtonsVisible && (<IconButtonGroupStyled isSingle withMargin>
174
+ { !blockedThread && !(blocked || needsApproval) && (<IconButton onClick={handleBlockClick} loading={blockItemMutation.isLoading} icon={<LockIcon />} label={getMessage('page.details.actions.comment.block', 'Block')} />)}
175
+ { !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} />)}
176
+ { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
177
+ <DiscussionThreadItemReviewAction
178
+ item={item}
179
+ queryToInvalidate="get-details-data"
180
+ areBlockButtonsDisabled={blockButtonsDisabled}
181
+ isLoading={isLoading}
182
+ allowedActions={{ canModerate, canAccessReports, canReviewReports }}
183
+ blockItemMutation={blockItemMutation}
184
+ blockItemThreadMutation={blockItemThreadMutation}
185
+ onBlockButtonsStateChange={handleBlockButtonsStateChange}
186
+ onBlockActionClick={handleBlockActionClick}
187
+ />
188
+ </IconButtonGroupStyled>)}
189
+ { hasActiveThread && (<IconButtonGroupStyled isSingle withMargin>
190
+ <IconButton
191
+ onClick={handleDrilldownClick}
192
+ icon={<Eye />}
193
+ label={getMessage('page.details.panel.discussion.nav.drilldown', 'Drilldown thread')}
194
+ style={(blocked && !blockedThread) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />
195
+ </IconButtonGroupStyled>) }
154
196
  </DiscussionThreadItemActionsWrapper>
155
197
  { !blocked && (<ConfirmationDialog
156
198
  isVisible={blockConfirmationVisible}
@@ -162,7 +204,7 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
162
204
  onCancel={handleBlockCancel}>
163
205
  { getMessage('page.details.actions.comment.block.confirmation.description') }
164
206
  </ConfirmationDialog>) }
165
- { (!blockedThread && root) && (<ConfirmationDialog
207
+ { !blockedThread && (<ConfirmationDialog
166
208
  isVisible={blockThreadConfirmationVisible}
167
209
  isActionAsync={blockItemThreadMutation.isLoading}
168
210
  header={getMessage('page.details.actions.thread.block.confirmation.header')}
@@ -1,18 +1,7 @@
1
1
  import styled from 'styled-components';
2
2
  import { Box } from '@strapi/design-system/Box';
3
- import { Badge } from '@strapi/design-system/Badge';
4
- import { IconButtonGroup } from '@strapi/design-system/IconButton';
5
-
6
- export const DiscussionThreadItemActionsBadge = styled(Badge)`
7
- border: 1px ${ ({ theme, color }) => theme.colors[`${color}200`] } solid;
8
- `;
9
3
 
10
4
  export const DiscussionThreadItemActionsWrapper = styled(Box)`
11
5
  margin-left: 1rem;
12
- align-items: start;
13
-
14
- ${DiscussionThreadItemActionsBadge} {
15
- margin-top: .175rem;
16
- margin-right: .5rem;
17
- }
6
+ align-items: flex-start;
18
7
  `;
@@ -11,7 +11,6 @@ import { IconButton } from '@strapi/design-system/IconButton';
11
11
  import { Check, Cross } from '@strapi/icons';
12
12
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
13
13
  import { getMessage, handleAPIError } from '../../utils';
14
- import { DiscussionThreadItemActionsGroupWrapper } from './styles';
15
14
  import { approveItem, rejectItem } from '../../pages/utils/api';
16
15
  import pluginId from '../../pluginId';
17
16
 
@@ -60,10 +59,10 @@ const DiscussionThreadItemApprovalFlowActions = ({ id, allowedActions: { canMode
60
59
  };
61
60
 
62
61
  if (canModerate) {
63
- return (<DiscussionThreadItemActionsGroupWrapper>
62
+ return (<>
64
63
  <IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
65
64
  <IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
66
- </DiscussionThreadItemActionsGroupWrapper>);
65
+ </>);
67
66
  }
68
67
  return null;
69
68
  };
@@ -0,0 +1,47 @@
1
+ /**
2
+ *
3
+ * Discussion thread - Item footer
4
+ *
5
+ */
6
+
7
+ import React from 'react';
8
+ import PropTypes from 'prop-types';
9
+ import { useIntl } from 'react-intl';
10
+ import { Avatar, Initials } from '@strapi/design-system/Avatar';
11
+ import { Box } from '@strapi/design-system/Box';
12
+ import { Typography } from '@strapi/design-system/Typography';
13
+ import { renderInitials } from '../../utils';
14
+ import { DiscussionThreadItemFooterMeta, DiscussionThreadItemFooterStyled } from './styles';
15
+
16
+ const DiscussionThreadItemFooter = ({ children, author, createdAt, updatedAt }) => {
17
+
18
+ const { formatDate } = useIntl();
19
+
20
+ const dateTime = formatDate(updatedAt || createdAt, { dateStyle: 'medium', timeStyle: 'short' });
21
+ const { name, avatar } = author;
22
+
23
+
24
+ return (<DiscussionThreadItemFooterStyled as={Box} paddingTop={2} direction="row">
25
+ <DiscussionThreadItemFooterMeta size={3} horizontal>
26
+ { avatar ? <Avatar src={avatar} alt={name} /> : <Initials>{ renderInitials(name) }</Initials> }
27
+ <Typography variant="pi" fontWeight="bold" textColor="neutral800">{ name }</Typography>
28
+ <Typography variant="pi" textColor="neutral600">{ dateTime }</Typography>
29
+ </DiscussionThreadItemFooterMeta>
30
+ { children }
31
+ </DiscussionThreadItemFooterStyled>);
32
+ };
33
+
34
+ DiscussionThreadItemFooter.propTypes = {
35
+ author: PropTypes.shape({
36
+ id: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired,
37
+ name: PropTypes.string.isRequired,
38
+ email: PropTypes.string,
39
+ avatar: PropTypes.string,
40
+ }).isRequired,
41
+ createdAt: PropTypes.string.isRequired,
42
+ updatedAt: PropTypes.string,
43
+ children: PropTypes.array,
44
+ };
45
+
46
+ export default DiscussionThreadItemFooter;
47
+
@@ -0,0 +1,13 @@
1
+ import styled from 'styled-components';
2
+ import { Flex } from '@strapi/design-system/Flex';
3
+ import { Stack } from '@strapi/design-system/Stack';
4
+
5
+ export const DiscussionThreadItemFooterStyled = styled(Flex)`
6
+ width: 100%;
7
+ `;
8
+
9
+ export const DiscussionThreadItemFooterMeta = styled(Stack)`
10
+ * + * {
11
+ margin-left: 8px;
12
+ }
13
+ `;