From 8e3006d3a3375339d4710386ab4c94cca865dfdc Mon Sep 17 00:00:00 2001 From: Mateusz Ziarko Date: Fri, 15 Apr 2022 14:55:54 +0200 Subject: [PATCH] feat: issue #113 author field population --- __mocks__/initSetup.ts | 2 +- admin/src/components/Avatar/index.tsx | 34 +++++++++++++++++ .../DiscussionThreadItemFooter/index.tsx | 11 ++---- .../components/DiscoverTableRow/index.tsx | 9 +---- server/controllers/utils/parsers.ts | 38 +++++++++++++++++-- server/services/__tests__/common.test.ts | 29 ++++++++++++-- server/services/admin.ts | 14 ++++--- server/services/common.ts | 11 ++++-- server/services/utils/functions.ts | 18 +++++---- types/contentTypes.ts | 8 ++-- types/services.d.ts | 2 +- 11 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 admin/src/components/Avatar/index.tsx diff --git a/__mocks__/initSetup.ts b/__mocks__/initSetup.ts index 66ff796..611597b 100644 --- a/__mocks__/initSetup.ts +++ b/__mocks__/initSetup.ts @@ -22,7 +22,7 @@ export = (config: any = {}, toStore: boolean = false, database: any = {}) => { const parseValues = (values: any[], args: any = {}) => values.map(_ => { const { select = [] } = args; if (!isEmpty(select)) { - return pick(_, [...select, 'threadOf', 'authorUser']); // Relation fields can't be "unselected" + return pick(_, [...select, 'threadOf']); } return _; }) diff --git a/admin/src/components/Avatar/index.tsx b/admin/src/components/Avatar/index.tsx new file mode 100644 index 0000000..3336575 --- /dev/null +++ b/admin/src/components/Avatar/index.tsx @@ -0,0 +1,34 @@ +/** + * + * Avatar + * + */ + +// @ts-nocheck + +import React from "react"; +import PropTypes from "prop-types"; +import { isObject } from "lodash"; +import { Avatar, Initials } from "@strapi/design-system/Avatar"; +import { renderInitials } from "../../utils"; + +const UserAvatar = ({ + avatar, + name, +}) => { + if (avatar) { + let image = avatar; + if (isObject(avatar)) { + image = avatar?.formats?.thumbnail.url || avatar.url + } + return + } + return {renderInitials(name)} +}; + +UserAvatar.propTypes = { + avatar: PropTypes.oneOfType(PropTypes.string, PropTypes.object).isRequired, + name: PropTypes.string, +}; + +export default UserAvatar; diff --git a/admin/src/components/DiscussionThreadItemFooter/index.tsx b/admin/src/components/DiscussionThreadItemFooter/index.tsx index 64d010b..024a885 100644 --- a/admin/src/components/DiscussionThreadItemFooter/index.tsx +++ b/admin/src/components/DiscussionThreadItemFooter/index.tsx @@ -9,14 +9,13 @@ import React from "react"; import PropTypes from "prop-types"; import { useIntl } from "react-intl"; -import { Avatar, Initials } from "@strapi/design-system/Avatar"; import { Box } from "@strapi/design-system/Box"; import { Typography } from "@strapi/design-system/Typography"; -import { renderInitials } from "../../utils"; import { DiscussionThreadItemFooterMeta, DiscussionThreadItemFooterStyled, } from "./styles"; +import UserAvatar from "../Avatar"; const DiscussionThreadItemFooter = ({ children, @@ -35,11 +34,7 @@ const DiscussionThreadItemFooter = ({ return ( - {avatar ? ( - - ) : ( - {renderInitials(name)} - )} + {name} @@ -57,7 +52,7 @@ DiscussionThreadItemFooter.propTypes = { id: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired, name: PropTypes.string.isRequired, email: PropTypes.string, - avatar: PropTypes.string, + avatar: PropTypes.oneOfType(PropTypes.string, PropTypes.object), }).isRequired, createdAt: PropTypes.string.isRequired, updatedAt: PropTypes.string, diff --git a/admin/src/pages/Discover/components/DiscoverTableRow/index.tsx b/admin/src/pages/Discover/components/DiscoverTableRow/index.tsx index 5098092..9a5f7ce 100644 --- a/admin/src/pages/Discover/components/DiscoverTableRow/index.tsx +++ b/admin/src/pages/Discover/components/DiscoverTableRow/index.tsx @@ -5,7 +5,6 @@ import PropTypes from "prop-types"; import { useIntl } from "react-intl"; import { useMutation, useQueryClient } from "react-query"; import { isNil, isEmpty } from "lodash"; -import { Avatar, Initials } from "@strapi/design-system/Avatar"; import { Flex } from "@strapi/design-system/Flex"; import { IconButton } from "@strapi/design-system/IconButton"; import { Link } from "@strapi/design-system/Link"; @@ -18,7 +17,6 @@ import { getMessage, getUrl, handleAPIError, - renderInitials, resolveCommentStatus, resolveCommentStatusColor, } from "../../../../utils"; @@ -31,6 +29,7 @@ import DiscussionThreadItemApprovalFlowActions from "../../../../components/Disc import StatusBadge from "../../../../components/StatusBadge"; import { IconButtonGroupStyled } from "../../../../components/IconButton/styles"; import DiscussionThreadItemReviewAction from "../../../../components/DiscussionThreadItemReviewAction"; +import UserAvatar from "../../../../components/Avatar"; const DiscoverTableRow = ({ config, @@ -154,11 +153,7 @@ const DiscoverTableRow = ({ - {item.author?.avatar ? ( - - ) : ( - {renderInitials(item.author.name)} - )} + {item.author.name} diff --git a/server/controllers/utils/parsers.ts b/server/controllers/utils/parsers.ts index 2fcc4fc..1c25a3e 100644 --- a/server/controllers/utils/parsers.ts +++ b/server/controllers/utils/parsers.ts @@ -8,10 +8,41 @@ export const flatInput = ( pagination?: ToBeFixed, fields?: StrapiRequestQueryFieldsClause>, ) => { - const filters = query?.filters || query; + const { populate = {}, filters } = query; const orOperator = (filters?.$or || []).filter( (_: ToBeFixed) => !Object.keys(_).includes("removed") ); + + let basePopulate = { + ...populate, + }; + + let threadOfPopulate = { + threadOf: { + populate: { + authorUser: true, + ...populate, + }, + }, + }; + + // Cover case when someone wants to populate author instead of authorUser + if (populate.author) { + const { author, ...restPopulate } = populate; + basePopulate = { + ...restPopulate, + authorUser: author + }; + threadOfPopulate = { + threadOf: { + populate: { + authorUser: author, + ...restPopulate, + }, + }, + }; + } + return { query: { ...filters, @@ -19,9 +50,8 @@ export const flatInput = ( related: relation, }, populate: { - threadOf: { - populate: { authorUser: true }, - }, + ...basePopulate, + ...threadOfPopulate, }, pagination, sort, diff --git a/server/services/__tests__/common.test.ts b/server/services/__tests__/common.test.ts index 82d93e7..ac3e45d 100644 --- a/server/services/__tests__/common.test.ts +++ b/server/services/__tests__/common.test.ts @@ -393,9 +393,15 @@ describe("Test Comments service functions utils", () => { content: "IKL", threadOf: 2, related, - authorId: 1, - authorName: "Joe Doe", - authorEmail: "joe@example.com", + authorUser: { + id: 1, + username: "Joe Doe", + email: "joe@example.com", + avatar: { + id: 1, + url: 'http://example.com' + } + } }, ]; const relatedEntity = { id: 1, title: "Test", uid: collection }; @@ -435,6 +441,23 @@ describe("Test Comments service functions utils", () => { expect(Object.keys(filterOutUndefined(result.data[3]))).toHaveLength(6); }); + test("Should return structure with populated avatar field", async () => { + const result = await getPluginService( + "common" + ).findAllFlat({ + query: { related }, + populate: { + authorUser: { + populate: { avatar: true }, + }, + } + }, relatedEntity); + expect(result).toHaveProperty("data"); + expect(result).not.toHaveProperty("meta"); + expect(result.data.length).toBe(4); + expect(result).toHaveProperty(["data", 3, "author", "avatar", "url"], db[3].authorUser.avatar.url); + }); + test("Should return structure with pagination", async () => { const result = await getPluginService( "common" diff --git a/server/services/admin.ts b/server/services/admin.ts index 6ce3d7b..7caa6b1 100644 --- a/server/services/admin.ts +++ b/server/services/admin.ts @@ -168,7 +168,7 @@ export = ({ strapi }: StrapiContext): IServiceAdmin => ({ .findMany({ ...params, populate: { - authorUser: true, + authorUser: { populate: { avatar: true } }, threadOf: true, reports: { where: { @@ -185,7 +185,7 @@ export = ({ strapi }: StrapiContext): IServiceAdmin => ({ const result = entities .map((_) => filterOurResolvedReports( - this.getCommonService().sanitizeCommentEntity(_) + this.getCommonService().sanitizeCommentEntity(_, { avatar: true }) ) ) .map((_) => @@ -226,10 +226,14 @@ export = ({ strapi }: StrapiContext): IServiceAdmin => ({ const defaultPopulate = { populate: { - authorUser: true, + authorUser: { + populate: { avatar: true } + }, threadOf: { populate: { - authorUser: true, + authorUser: { + populate: { avatar: true } + }, ...reportsPopulation, }, }, @@ -288,7 +292,7 @@ export = ({ strapi }: StrapiContext): IServiceAdmin => ({ const selectedEntity = this.getCommonService().sanitizeCommentEntity({ ...entity, threadOf: entity.threadOf || null, - }); + }, { avatar: true }); return { entity: relatedEntity, diff --git a/server/services/common.ts b/server/services/common.ts index 780a2ae..3d4b095 100644 --- a/server/services/common.ts +++ b/server/services/common.ts @@ -20,6 +20,7 @@ import { StrapiResponseMeta, StrapiPaginatedResponse, StrapiDBQueryArgs, + StringMap, } from "strapi-typed"; import { CommentsPluginConfig, @@ -240,7 +241,7 @@ export = ({ strapi }: StrapiContext): IServiceCommon => ({ threadOf: parsedThreadOf || _.threadOf || null, gotThread: (threadedItem?.itemsInTread || 0) > 0, threadFirstItemId: threadedItem?.firstThreadItemId, - }); + }, populate?.authorUser?.populate); }); return { @@ -386,14 +387,16 @@ export = ({ strapi }: StrapiContext): IServiceCommon => ({ } }, - sanitizeCommentEntity(entity: Comment): Comment { + sanitizeCommentEntity(entity: Comment, populate?: StringMap | StringMap>): Comment { + const fieldsToPopulate = isArray(populate) ? populate : Object.keys(populate || {}); + return { ...buildAuthorModel({ ...entity, threadOf: isObject(entity.threadOf) - ? buildAuthorModel(entity.threadOf) + ? buildAuthorModel(entity.threadOf, fieldsToPopulate) : entity.threadOf, - }), + }, fieldsToPopulate), }; }, diff --git a/server/services/utils/functions.ts b/server/services/utils/functions.ts index 8e13ad0..237f637 100644 --- a/server/services/utils/functions.ts +++ b/server/services/utils/functions.ts @@ -94,7 +94,7 @@ export const convertContentTypeNameToSlug = (str: string): string => { : plainConversion; }; -export const buildAuthorModel = (item: Comment): Comment => { +export const buildAuthorModel = (item: Comment, fieldsToPopulate: Array = []): Comment => { const { authorUser, authorId, @@ -105,12 +105,16 @@ export const buildAuthorModel = (item: Comment): Comment => { } = item; let author: CommentAuthor = {} as CommentAuthor; if (authorUser) { - author = { - id: authorUser.id, - name: authorUser.username, - email: authorUser.email, - avatar: authorUser.avatar, - }; + author = fieldsToPopulate + .reduce((prev, curr) => ({ + ...prev, + [curr]: authorUser[curr], + }), { + id: authorUser.id, + name: authorUser.username, + email: authorUser.email, + avatar: authorUser.avatar, + }); } else if (authorId) { author = { id: authorId, diff --git a/types/contentTypes.ts b/types/contentTypes.ts index 0a563e3..8f814c5 100644 --- a/types/contentTypes.ts +++ b/types/contentTypes.ts @@ -1,11 +1,11 @@ -import { StrapiUser } from "strapi-typed"; +import { StrapiUser, StringMap } from "strapi-typed"; export type Id = number | string; -export type Comment = { +export type Comment = { id: Id; content: string; - author?: CommentAuthor | undefined; + author?: TAuthor | undefined; children?: Array; reports?: Array; threadOf: Comment | number | null; @@ -38,6 +38,8 @@ export type CommentAuthorPartial = { authorUser?: StrapiUser; }; +export type CommentAuthorResolved = CommentAuthor & StringMap; + export type CommentReport = { id: Id; related: Comment | Id; diff --git a/types/services.d.ts b/types/services.d.ts index 5b15bd3..a44164c 100644 --- a/types/services.d.ts +++ b/types/services.d.ts @@ -92,7 +92,7 @@ export interface IServiceCommon { fieldName: string, value: any ): Promise; - sanitizeCommentEntity(entity: Comment): Comment; + sanitizeCommentEntity(entity: Comment, populate?: StringMap | StringMap>): Comment; isValidUserContext(user?: any): boolean; parseRelationString( relation: string