diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 021d747b..973b30df 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,11 @@ # @baseapp-frontend/components +## 0.0.23 + +### Patch Changes + +- Create a component for listing the members of a profile + ## 0.0.22 ### Patch Changes diff --git a/packages/components/__generated__/MemberItemFragment.graphql.ts b/packages/components/__generated__/MemberItemFragment.graphql.ts new file mode 100644 index 00000000..e82bc5cc --- /dev/null +++ b/packages/components/__generated__/MemberItemFragment.graphql.ts @@ -0,0 +1,97 @@ +/** + * @generated SignedSource<<15dc9b53215c47ddbaac982d5c81d426>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ + +/* eslint-disable */ +// @ts-nocheck +import { Fragment, ReaderFragment } from 'relay-runtime' +import { FragmentRefs } from 'relay-runtime' + +export type ProfileRoleStatus = 'ACTIVE' | 'INACTIVE' | 'PENDING' | '%future added value' +export type ProfileRoles = 'ADMIN' | 'MANAGER' | '%future added value' + +export type MemberItemFragment$data = { + readonly id: string + readonly role: ProfileRoles | null | undefined + readonly status: ProfileRoleStatus | null | undefined + readonly user: { + readonly profile: + | { + readonly ' $fragmentSpreads': FragmentRefs<'ProfileItemFragment'> + } + | null + | undefined + } + readonly ' $fragmentType': 'MemberItemFragment' +} +export type MemberItemFragment$key = { + readonly ' $data'?: MemberItemFragment$data + readonly ' $fragmentSpreads': FragmentRefs<'MemberItemFragment'> +} + +const node: ReaderFragment = { + argumentDefinitions: [], + kind: 'Fragment', + metadata: null, + name: 'MemberItemFragment', + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'id', + storageKey: null, + }, + { + alias: null, + args: null, + concreteType: 'User', + kind: 'LinkedField', + name: 'user', + plural: false, + selections: [ + { + alias: null, + args: null, + concreteType: 'Profile', + kind: 'LinkedField', + name: 'profile', + plural: false, + selections: [ + { + args: null, + kind: 'FragmentSpread', + name: 'ProfileItemFragment', + }, + ], + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'role', + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'status', + storageKey: null, + }, + ], + type: 'ProfileUserRole', + abstractKey: null, +} + +;(node as any).hash = 'bd85958690e77e1ccd3a6cc89ce44335' + +export default node diff --git a/packages/components/__generated__/UserMembersListFragment.graphql.ts b/packages/components/__generated__/UserMembersListFragment.graphql.ts new file mode 100644 index 00000000..325d383b --- /dev/null +++ b/packages/components/__generated__/UserMembersListFragment.graphql.ts @@ -0,0 +1,205 @@ +/** + * @generated SignedSource<<7b5d9ac9387460075ed09089a27cd807>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ + +/* eslint-disable */ +// @ts-nocheck +import { ReaderFragment, RefetchableFragment } from 'relay-runtime' +import { FragmentRefs } from 'relay-runtime' + +export type UserMembersListFragment$data = { + readonly id: string + readonly members: + | { + readonly edges: ReadonlyArray< + | { + readonly node: + | { + readonly ' $fragmentSpreads': FragmentRefs<'MemberItemFragment'> + } + | null + | undefined + } + | null + | undefined + > + readonly pageInfo: { + readonly endCursor: string | null | undefined + readonly hasNextPage: boolean + } + readonly totalCount: number | null | undefined + } + | null + | undefined + readonly ' $fragmentSpreads': FragmentRefs<'ProfileItemFragment'> + readonly ' $fragmentType': 'UserMembersListFragment' +} +export type UserMembersListFragment$key = { + readonly ' $data'?: UserMembersListFragment$data + readonly ' $fragmentSpreads': FragmentRefs<'UserMembersListFragment'> +} + +const node: ReaderFragment = (function () { + var v0 = ['members'] + return { + argumentDefinitions: [ + { + defaultValue: 10, + kind: 'LocalArgument', + name: 'count', + }, + { + defaultValue: null, + kind: 'LocalArgument', + name: 'cursor', + }, + { + defaultValue: null, + kind: 'LocalArgument', + name: 'orderBy', + }, + ], + kind: 'Fragment', + metadata: { + connection: [ + { + count: 'count', + cursor: 'cursor', + direction: 'forward', + path: v0 /*: any*/, + }, + ], + refetch: { + connection: { + forward: { + count: 'count', + cursor: 'cursor', + }, + backward: null, + path: v0 /*: any*/, + }, + fragmentPathInResult: ['node'], + operation: require('./userMembersListPaginationRefetchable.graphql'), + identifierInfo: { + identifierField: 'id', + identifierQueryVariableName: 'id', + }, + }, + }, + name: 'UserMembersListFragment', + selections: [ + { + args: null, + kind: 'FragmentSpread', + name: 'ProfileItemFragment', + }, + { + alias: 'members', + args: [ + { + kind: 'Variable', + name: 'orderBy', + variableName: 'orderBy', + }, + ], + concreteType: 'ProfileUserRoleConnection', + kind: 'LinkedField', + name: '__UserMembersFragment_members_connection', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'totalCount', + storageKey: null, + }, + { + alias: null, + args: null, + concreteType: 'ProfileUserRoleEdge', + kind: 'LinkedField', + name: 'edges', + plural: true, + selections: [ + { + alias: null, + args: null, + concreteType: 'ProfileUserRole', + kind: 'LinkedField', + name: 'node', + plural: false, + selections: [ + { + args: null, + kind: 'FragmentSpread', + name: 'MemberItemFragment', + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: '__typename', + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'cursor', + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: null, + concreteType: 'PageInfo', + kind: 'LinkedField', + name: 'pageInfo', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'endCursor', + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'hasNextPage', + storageKey: null, + }, + ], + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'id', + storageKey: null, + }, + ], + type: 'Profile', + abstractKey: null, + } +})() + +;(node as any).hash = 'b14044279dce0f71bd144a09351f0df2' + +export default node diff --git a/packages/components/__generated__/UserMembersListPaginationQuery.graphql.ts b/packages/components/__generated__/UserMembersListPaginationQuery.graphql.ts new file mode 100644 index 00000000..1e98f54f --- /dev/null +++ b/packages/components/__generated__/UserMembersListPaginationQuery.graphql.ts @@ -0,0 +1,353 @@ +/** + * @generated SignedSource<<5217aecc6cf81c52b03e9d986eff97c4>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ + +/* eslint-disable */ +// @ts-nocheck +import { ConcreteRequest, Query } from 'relay-runtime' +import { FragmentRefs } from 'relay-runtime' + +export type UserMembersListPaginationQuery$variables = { + count?: number | null | undefined + cursor?: string | null | undefined + orderBy?: string | null | undefined + profileId: string +} +export type UserMembersListPaginationQuery$data = { + readonly profile: + | { + readonly pk: number + readonly ' $fragmentSpreads': FragmentRefs<'UserMembersListFragment'> + } + | null + | undefined +} +export type UserMembersListPaginationQuery = { + response: UserMembersListPaginationQuery$data + variables: UserMembersListPaginationQuery$variables +} + +const node: ConcreteRequest = (function () { + var v0 = [ + { + defaultValue: 10, + kind: 'LocalArgument', + name: 'count', + }, + { + defaultValue: null, + kind: 'LocalArgument', + name: 'cursor', + }, + { + defaultValue: null, + kind: 'LocalArgument', + name: 'orderBy', + }, + { + defaultValue: null, + kind: 'LocalArgument', + name: 'profileId', + }, + ], + v1 = [ + { + kind: 'Variable', + name: 'id', + variableName: 'profileId', + }, + ], + v2 = { + alias: null, + args: null, + kind: 'ScalarField', + name: 'pk', + storageKey: null, + }, + v3 = { + kind: 'Variable', + name: 'orderBy', + variableName: 'orderBy', + }, + v4 = { + alias: null, + args: null, + kind: 'ScalarField', + name: 'id', + storageKey: null, + }, + v5 = { + alias: null, + args: null, + kind: 'ScalarField', + name: 'name', + storageKey: null, + }, + v6 = { + alias: null, + args: [ + { + kind: 'Literal', + name: 'height', + value: 100, + }, + { + kind: 'Literal', + name: 'width', + value: 100, + }, + ], + concreteType: 'File', + kind: 'LinkedField', + name: 'image', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'url', + storageKey: null, + }, + ], + storageKey: 'image(height:100,width:100)', + }, + v7 = { + alias: null, + args: null, + concreteType: 'URLPath', + kind: 'LinkedField', + name: 'urlPath', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'path', + storageKey: null, + }, + v4 /*: any*/, + ], + storageKey: null, + }, + v8 = [ + { + kind: 'Variable', + name: 'after', + variableName: 'cursor', + }, + { + kind: 'Variable', + name: 'first', + variableName: 'count', + }, + v3 /*: any*/, + ] + return { + fragment: { + argumentDefinitions: v0 /*: any*/, + kind: 'Fragment', + metadata: null, + name: 'UserMembersListPaginationQuery', + selections: [ + { + alias: null, + args: v1 /*: any*/, + concreteType: 'Profile', + kind: 'LinkedField', + name: 'profile', + plural: false, + selections: [ + v2 /*: any*/, + { + args: [ + { + kind: 'Variable', + name: 'count', + variableName: 'count', + }, + { + kind: 'Variable', + name: 'cursor', + variableName: 'cursor', + }, + v3 /*: any*/, + ], + kind: 'FragmentSpread', + name: 'UserMembersListFragment', + }, + ], + storageKey: null, + }, + ], + type: 'Query', + abstractKey: null, + }, + kind: 'Request', + operation: { + argumentDefinitions: v0 /*: any*/, + kind: 'Operation', + name: 'UserMembersListPaginationQuery', + selections: [ + { + alias: null, + args: v1 /*: any*/, + concreteType: 'Profile', + kind: 'LinkedField', + name: 'profile', + plural: false, + selections: [ + v2 /*: any*/, + v4 /*: any*/, + v5 /*: any*/, + v6 /*: any*/, + v7 /*: any*/, + { + alias: null, + args: v8 /*: any*/, + concreteType: 'ProfileUserRoleConnection', + kind: 'LinkedField', + name: 'members', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'totalCount', + storageKey: null, + }, + { + alias: null, + args: null, + concreteType: 'ProfileUserRoleEdge', + kind: 'LinkedField', + name: 'edges', + plural: true, + selections: [ + { + alias: null, + args: null, + concreteType: 'ProfileUserRole', + kind: 'LinkedField', + name: 'node', + plural: false, + selections: [ + v4 /*: any*/, + { + alias: null, + args: null, + concreteType: 'User', + kind: 'LinkedField', + name: 'user', + plural: false, + selections: [ + { + alias: null, + args: null, + concreteType: 'Profile', + kind: 'LinkedField', + name: 'profile', + plural: false, + selections: [v4 /*: any*/, v5 /*: any*/, v6 /*: any*/, v7 /*: any*/], + storageKey: null, + }, + v4 /*: any*/, + ], + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'role', + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'status', + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: '__typename', + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'cursor', + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: null, + concreteType: 'PageInfo', + kind: 'LinkedField', + name: 'pageInfo', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'endCursor', + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'hasNextPage', + storageKey: null, + }, + ], + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: v8 /*: any*/, + filters: ['orderBy'], + handle: 'connection', + key: 'UserMembersFragment_members', + kind: 'LinkedHandle', + name: 'members', + }, + ], + storageKey: null, + }, + ], + }, + params: { + cacheID: 'e889baf91ade9a8612108dfa2583a2c8', + id: null, + metadata: {}, + name: 'UserMembersListPaginationQuery', + operationKind: 'query', + text: 'query UserMembersListPaginationQuery(\n $count: Int = 10\n $cursor: String\n $orderBy: String\n $profileId: ID!\n) {\n profile(id: $profileId) {\n pk\n ...UserMembersListFragment_32czeo\n id\n }\n}\n\nfragment MemberItemFragment on ProfileUserRole {\n id\n user {\n profile {\n ...ProfileItemFragment\n id\n }\n id\n }\n role\n status\n}\n\nfragment ProfileItemFragment on Profile {\n id\n name\n image(width: 100, height: 100) {\n url\n }\n urlPath {\n path\n id\n }\n}\n\nfragment UserMembersListFragment_32czeo on Profile {\n ...ProfileItemFragment\n members(first: $count, after: $cursor, orderBy: $orderBy) {\n totalCount\n edges {\n node {\n ...MemberItemFragment\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n', + }, + } +})() + +;(node as any).hash = 'bc02b9d42e6f3e087d064c23c98c1a16' + +export default node diff --git a/packages/components/__generated__/userMembersListPaginationRefetchable.graphql.ts b/packages/components/__generated__/userMembersListPaginationRefetchable.graphql.ts new file mode 100644 index 00000000..fc831d2f --- /dev/null +++ b/packages/components/__generated__/userMembersListPaginationRefetchable.graphql.ts @@ -0,0 +1,355 @@ +/** + * @generated SignedSource<<7fae0b75bc4bce4bae590e668afd8d37>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ + +/* eslint-disable */ +// @ts-nocheck +import { ConcreteRequest, Query } from 'relay-runtime' +import { FragmentRefs } from 'relay-runtime' + +export type userMembersListPaginationRefetchable$variables = { + count?: number | null | undefined + cursor?: string | null | undefined + id: string + orderBy?: string | null | undefined +} +export type userMembersListPaginationRefetchable$data = { + readonly node: + | { + readonly ' $fragmentSpreads': FragmentRefs<'UserMembersListFragment'> + } + | null + | undefined +} +export type userMembersListPaginationRefetchable = { + response: userMembersListPaginationRefetchable$data + variables: userMembersListPaginationRefetchable$variables +} + +const node: ConcreteRequest = (function () { + var v0 = { + defaultValue: 10, + kind: 'LocalArgument', + name: 'count', + }, + v1 = { + defaultValue: null, + kind: 'LocalArgument', + name: 'cursor', + }, + v2 = { + defaultValue: null, + kind: 'LocalArgument', + name: 'id', + }, + v3 = { + defaultValue: null, + kind: 'LocalArgument', + name: 'orderBy', + }, + v4 = [ + { + kind: 'Variable', + name: 'id', + variableName: 'id', + }, + ], + v5 = { + kind: 'Variable', + name: 'orderBy', + variableName: 'orderBy', + }, + v6 = { + alias: null, + args: null, + kind: 'ScalarField', + name: '__typename', + storageKey: null, + }, + v7 = { + alias: null, + args: null, + kind: 'ScalarField', + name: 'id', + storageKey: null, + }, + v8 = { + alias: null, + args: null, + kind: 'ScalarField', + name: 'name', + storageKey: null, + }, + v9 = { + alias: null, + args: [ + { + kind: 'Literal', + name: 'height', + value: 100, + }, + { + kind: 'Literal', + name: 'width', + value: 100, + }, + ], + concreteType: 'File', + kind: 'LinkedField', + name: 'image', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'url', + storageKey: null, + }, + ], + storageKey: 'image(height:100,width:100)', + }, + v10 = { + alias: null, + args: null, + concreteType: 'URLPath', + kind: 'LinkedField', + name: 'urlPath', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'path', + storageKey: null, + }, + v7 /*: any*/, + ], + storageKey: null, + }, + v11 = [ + { + kind: 'Variable', + name: 'after', + variableName: 'cursor', + }, + { + kind: 'Variable', + name: 'first', + variableName: 'count', + }, + v5 /*: any*/, + ] + return { + fragment: { + argumentDefinitions: [v0 /*: any*/, v1 /*: any*/, v2 /*: any*/, v3 /*: any*/], + kind: 'Fragment', + metadata: null, + name: 'userMembersListPaginationRefetchable', + selections: [ + { + alias: null, + args: v4 /*: any*/, + concreteType: null, + kind: 'LinkedField', + name: 'node', + plural: false, + selections: [ + { + args: [ + { + kind: 'Variable', + name: 'count', + variableName: 'count', + }, + { + kind: 'Variable', + name: 'cursor', + variableName: 'cursor', + }, + v5 /*: any*/, + ], + kind: 'FragmentSpread', + name: 'UserMembersListFragment', + }, + ], + storageKey: null, + }, + ], + type: 'Query', + abstractKey: null, + }, + kind: 'Request', + operation: { + argumentDefinitions: [v0 /*: any*/, v1 /*: any*/, v3 /*: any*/, v2 /*: any*/], + kind: 'Operation', + name: 'userMembersListPaginationRefetchable', + selections: [ + { + alias: null, + args: v4 /*: any*/, + concreteType: null, + kind: 'LinkedField', + name: 'node', + plural: false, + selections: [ + v6 /*: any*/, + v7 /*: any*/, + { + kind: 'InlineFragment', + selections: [ + v8 /*: any*/, + v9 /*: any*/, + v10 /*: any*/, + { + alias: null, + args: v11 /*: any*/, + concreteType: 'ProfileUserRoleConnection', + kind: 'LinkedField', + name: 'members', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'totalCount', + storageKey: null, + }, + { + alias: null, + args: null, + concreteType: 'ProfileUserRoleEdge', + kind: 'LinkedField', + name: 'edges', + plural: true, + selections: [ + { + alias: null, + args: null, + concreteType: 'ProfileUserRole', + kind: 'LinkedField', + name: 'node', + plural: false, + selections: [ + v7 /*: any*/, + { + alias: null, + args: null, + concreteType: 'User', + kind: 'LinkedField', + name: 'user', + plural: false, + selections: [ + { + alias: null, + args: null, + concreteType: 'Profile', + kind: 'LinkedField', + name: 'profile', + plural: false, + selections: [ + v7 /*: any*/, + v8 /*: any*/, + v9 /*: any*/, + v10 /*: any*/, + ], + storageKey: null, + }, + v7 /*: any*/, + ], + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'role', + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'status', + storageKey: null, + }, + v6 /*: any*/, + ], + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'cursor', + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: null, + concreteType: 'PageInfo', + kind: 'LinkedField', + name: 'pageInfo', + plural: false, + selections: [ + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'endCursor', + storageKey: null, + }, + { + alias: null, + args: null, + kind: 'ScalarField', + name: 'hasNextPage', + storageKey: null, + }, + ], + storageKey: null, + }, + ], + storageKey: null, + }, + { + alias: null, + args: v11 /*: any*/, + filters: ['orderBy'], + handle: 'connection', + key: 'UserMembersFragment_members', + kind: 'LinkedHandle', + name: 'members', + }, + ], + type: 'Profile', + abstractKey: null, + }, + ], + storageKey: null, + }, + ], + }, + params: { + cacheID: '3f7d21cee41fbf8a15113dc8de609c61', + id: null, + metadata: {}, + name: 'userMembersListPaginationRefetchable', + operationKind: 'query', + text: 'query userMembersListPaginationRefetchable(\n $count: Int = 10\n $cursor: String\n $orderBy: String\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...UserMembersListFragment_32czeo\n id\n }\n}\n\nfragment MemberItemFragment on ProfileUserRole {\n id\n user {\n profile {\n ...ProfileItemFragment\n id\n }\n id\n }\n role\n status\n}\n\nfragment ProfileItemFragment on Profile {\n id\n name\n image(width: 100, height: 100) {\n url\n }\n urlPath {\n path\n id\n }\n}\n\nfragment UserMembersListFragment_32czeo on Profile {\n ...ProfileItemFragment\n members(first: $count, after: $cursor, orderBy: $orderBy) {\n totalCount\n edges {\n node {\n ...MemberItemFragment\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n', + }, + } +})() + +;(node as any).hash = 'b14044279dce0f71bd144a09351f0df2' + +export default node diff --git a/packages/components/modules/profiles/Members/MemberItem/index.tsx b/packages/components/modules/profiles/Members/MemberItem/index.tsx new file mode 100644 index 00000000..e3a07553 --- /dev/null +++ b/packages/components/modules/profiles/Members/MemberItem/index.tsx @@ -0,0 +1,56 @@ +import { FC } from 'react' + +import { AvatarWithPlaceholder } from '@baseapp-frontend/design-system' + +import { Box, Button, Typography } from '@mui/material' +import { useFragment } from 'react-relay' + +import { ProfileItemFragment$key } from '../../../../__generated__/ProfileItemFragment.graphql' +import { ProfileItemFragment } from '../../graphql/queries/ProfileItem' +import { MemberStatuses } from '../constants' +import { capitalizeFirstLetter } from '../utils' +import { MemberItemContainer, MemberPersonalInformation } from './styled' +import { MemberItemProps } from './types' + +const MemberItem: FC = ({ + member, + memberRole, + status, + avatarProps = {}, + avatarWidth = 40, + avatarHeight = 40, +}) => { + const memberProfile = useFragment(ProfileItemFragment, member) + const hasStatusAndRole = status && memberRole + if (!memberProfile) return null + return ( + + + + + {memberProfile.name} + {memberProfile?.urlPath?.path} + + + + {hasStatusAndRole && ( + + + + )} + + ) +} + +export default MemberItem diff --git a/packages/components/modules/profiles/Members/MemberItem/styled.tsx b/packages/components/modules/profiles/Members/MemberItem/styled.tsx new file mode 100644 index 00000000..17f424c0 --- /dev/null +++ b/packages/components/modules/profiles/Members/MemberItem/styled.tsx @@ -0,0 +1,21 @@ +import { Box, styled } from '@mui/material' + +import { MemberPersonalInformationProps } from './types' + +export const MemberItemContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1.5), + alignItems: 'center', + justifyContent: 'space-between', + padding: theme.spacing(1.5, 0), +})) + +export const MemberPersonalInformation = styled(Box, { + shouldForwardProp: (prop) => prop !== 'isActive', +})(({ isActive, theme }) => ({ + opacity: isActive ? 1 : 0.6, + display: 'flex', + gap: theme.spacing(1.5), + alignItems: 'center', + justifyContent: 'space-between', +})) diff --git a/packages/components/modules/profiles/Members/MemberItem/types.ts b/packages/components/modules/profiles/Members/MemberItem/types.ts new file mode 100644 index 00000000..072a903e --- /dev/null +++ b/packages/components/modules/profiles/Members/MemberItem/types.ts @@ -0,0 +1,17 @@ +import type { AvatarProps, BoxProps } from '@mui/material' + +import { MemberItemFragment$data } from '../../../../__generated__/MemberItemFragment.graphql' +import type { ProfileItemFragment$key } from '../../../../__generated__/ProfileItemFragment.graphql' + +export interface MemberPersonalInformationProps extends BoxProps { + isActive: boolean +} + +export interface MemberItemProps { + member: ProfileItemFragment$key | null | undefined + memberRole: MemberItemFragment$data['role'] | 'owner' + status: MemberItemFragment$data['status'] + avatarProps?: AvatarProps + avatarWidth?: number + avatarHeight?: number +} diff --git a/packages/components/modules/profiles/Members/MemberListItem/index.tsx b/packages/components/modules/profiles/Members/MemberListItem/index.tsx new file mode 100644 index 00000000..4c9373e7 --- /dev/null +++ b/packages/components/modules/profiles/Members/MemberListItem/index.tsx @@ -0,0 +1,82 @@ +import { FC } from 'react' + +import { Divider } from '@mui/material' +import { useFragment } from 'react-relay' + +import { MemberItemFragment } from '../../graphql/queries/MemberItem' +import { MemberStatuses } from '../constants' +import { MemberListItemProps } from './types' + +const MemberListItem: FC = ({ + member, + data, + prevMember, + nextMember, + MemberItemComponent, + memberItemComponentProps, +}) => { + const memberFragment = useFragment(MemberItemFragment, member) + const nextMemberFragment = useFragment(MemberItemFragment, nextMember) + const prevMemberFragment = useFragment(MemberItemFragment, prevMember) + + const isActiveMember = memberFragment.status === MemberStatuses.active + const isPreviousMemberInactive = prevMemberFragment?.status !== MemberStatuses.active + const isPreviousMemberUndefined = !prevMemberFragment?.status + const isNextMemberUndefined = !nextMemberFragment?.status + const isFirstActiveMember = + (isActiveMember && isPreviousMemberInactive) || (isActiveMember && isPreviousMemberUndefined) + const isLastMemberInactive = !isActiveMember && isNextMemberUndefined + + if (!memberFragment) return null + + if (isFirstActiveMember) { + return ( + <> + + + + + ) + } + + if (isLastMemberInactive) { + return ( + <> + + + + + ) + } + + return ( + + ) +} + +export default MemberListItem diff --git a/packages/components/modules/profiles/Members/MemberListItem/types.ts b/packages/components/modules/profiles/Members/MemberListItem/types.ts new file mode 100644 index 00000000..a86305b8 --- /dev/null +++ b/packages/components/modules/profiles/Members/MemberListItem/types.ts @@ -0,0 +1,14 @@ +import { FC } from 'react' + +import { MemberItemFragment$key } from '../../../../__generated__/MemberItemFragment.graphql' +import { UserMembersListFragment$data } from '../../../../__generated__/UserMembersListFragment.graphql' +import { MemberItemProps } from '../MemberItem/types' + +export interface MemberListItemProps { + member: MemberItemFragment$key + data: UserMembersListFragment$data + prevMember: MemberItemFragment$key | null | undefined + nextMember: MemberItemFragment$key | null | undefined + MemberItemComponent: FC + memberItemComponentProps?: Partial +} diff --git a/packages/components/modules/profiles/Members/MembersList/index.tsx b/packages/components/modules/profiles/Members/MembersList/index.tsx new file mode 100644 index 00000000..2c82d0ab --- /dev/null +++ b/packages/components/modules/profiles/Members/MembersList/index.tsx @@ -0,0 +1,90 @@ +import { FC, useMemo } from 'react' + +import { LoadingState as DefaultLoadingState } from '@baseapp-frontend/design-system' + +import { Box, Typography } from '@mui/material' +import { usePaginationFragment } from 'react-relay' +import { Virtuoso } from 'react-virtuoso' + +import { MemberItemFragment$key } from '../../../../__generated__/MemberItemFragment.graphql' +import { UserMembersListFragment } from '../../graphql/queries/UserMembersList' +import DefaultMemberItem from '../MemberItem' +import MemberListItem from '../MemberListItem' +import { MemberStatuses, NUMBER_OF_MEMBERS_TO_LOAD_NEXT } from '../constants' +import { MemberListProps } from '../types' + +const MembersList: FC = ({ + userRef, + MemberItem = DefaultMemberItem, + MemberItemProps = {}, + LoadingState = DefaultLoadingState, + LoadingStateProps = {}, + membersContainerHeight = 400, +}) => { + const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment( + UserMembersListFragment, + userRef, + ) + + const members = useMemo( + () => data?.members?.edges.filter((edge) => edge?.node).map((edge) => edge?.node) || [], + [data?.members?.edges], + ) + + const renderLoadingState = () => { + if (!isLoadingNext) return + + return ( + + ) + } + + const renderMemberItem = (member: MemberItemFragment$key, index: number) => ( + + ) + + if (members.length === 0) { + return ( + <> + + 1 member + + + + ) + } + + return ( + <> + + {(data.members?.totalCount ?? 0) + 1} members + + member && renderMemberItem(member, _index)} + components={{ + Footer: renderLoadingState, + }} + endReached={() => { + if (hasNext) { + loadNext(NUMBER_OF_MEMBERS_TO_LOAD_NEXT) + } + }} + /> + + ) +} + +export default MembersList diff --git a/packages/components/modules/profiles/Members/constants.ts b/packages/components/modules/profiles/Members/constants.ts new file mode 100644 index 00000000..20dc5b35 --- /dev/null +++ b/packages/components/modules/profiles/Members/constants.ts @@ -0,0 +1,7 @@ +export const NUMBER_OF_MEMBERS_TO_LOAD_NEXT = 5 +export const NUMBER_OF_MEMBERS_ON_FIRST_LOAD = 10 +export enum MemberStatuses { + active = 'ACTIVE', + pending = 'PENDING', + inactive = 'INACTIVE', +} diff --git a/packages/components/modules/profiles/Members/index.tsx b/packages/components/modules/profiles/Members/index.tsx new file mode 100644 index 00000000..4f6b3070 --- /dev/null +++ b/packages/components/modules/profiles/Members/index.tsx @@ -0,0 +1,69 @@ +import { FC, Suspense } from 'react' + +import { LoadingState as DefaultLoadingState } from '@baseapp-frontend/design-system' + +import { Typography } from '@mui/material' +import { useLazyLoadQuery } from 'react-relay' + +import { UserMembersListPaginationQuery as IUserMembersListPaginationQuery } from '../../../__generated__/UserMembersListPaginationQuery.graphql' +import useCurrentProfile from '../context/useCurrentProfile' +import { UserMembersListPaginationQuery } from '../graphql/queries/UserMembersList' +import DefaultMemberItem from './MemberItem' +import MembersList from './MembersList' +import { NUMBER_OF_MEMBERS_ON_FIRST_LOAD } from './constants' +import { UserMembersProps, UserMembersSuspendedProps } from './types' + +const Members: FC = ({ + MemberItem, + LoadingState, + LoadingStateProps, + membersContainerHeight, +}) => { + const { profile: currentProfile } = useCurrentProfile() + + const data = useLazyLoadQuery(UserMembersListPaginationQuery, { + profileId: currentProfile?.id || '', + count: NUMBER_OF_MEMBERS_ON_FIRST_LOAD, + orderBy: 'status', + }) + + if (!data.profile) return null + return ( + + ) +} + +const MembersSuspended: FC = ({ + title = 'Members', + subtitle, + MemberItem = DefaultMemberItem, + LoadingState = DefaultLoadingState, + LoadingStateProps = {}, + InitialLoadingState = DefaultLoadingState, + membersContainerHeight = 400, +}) => ( + <> + + {title} + + + {subtitle} + + }> + + + +) + +export default MembersSuspended diff --git a/packages/components/modules/profiles/Members/types.ts b/packages/components/modules/profiles/Members/types.ts new file mode 100644 index 00000000..b1c06a40 --- /dev/null +++ b/packages/components/modules/profiles/Members/types.ts @@ -0,0 +1,27 @@ +import type { FC } from 'react' + +import type { LoadingStateProps } from '@baseapp-frontend/design-system' + +import type { UserMembersListFragment$key } from '../../../__generated__/UserMembersListFragment.graphql' +import type { MemberItemProps } from './MemberItem/types' + +export interface MemberListProps { + MemberItem: FC + MemberItemProps?: Partial + userRef: UserMembersListFragment$key + LoadingState: FC + LoadingStateProps: LoadingStateProps + membersContainerHeight?: number +} + +export interface UserMembersSuspendedProps { + MemberItem?: FC + LoadingState?: FC + LoadingStateProps?: LoadingStateProps + title?: string + subtitle?: string + InitialLoadingState?: FC + membersContainerHeight?: number +} + +export interface UserMembersProps extends Omit {} diff --git a/packages/components/modules/profiles/Members/utils.ts b/packages/components/modules/profiles/Members/utils.ts new file mode 100644 index 00000000..fe8410c0 --- /dev/null +++ b/packages/components/modules/profiles/Members/utils.ts @@ -0,0 +1,2 @@ +export const capitalizeFirstLetter = (string: string) => + string.charAt(0).toUpperCase() + string.slice(1).toLowerCase() diff --git a/packages/components/modules/profiles/graphql/queries/MemberItem.ts b/packages/components/modules/profiles/graphql/queries/MemberItem.ts new file mode 100644 index 00000000..9851ef27 --- /dev/null +++ b/packages/components/modules/profiles/graphql/queries/MemberItem.ts @@ -0,0 +1,14 @@ +import { graphql } from 'react-relay' + +export const MemberItemFragment = graphql` + fragment MemberItemFragment on ProfileUserRole { + id + user { + profile { + ...ProfileItemFragment + } + } + role + status + } +` diff --git a/packages/components/modules/profiles/graphql/queries/UserMembersList.ts b/packages/components/modules/profiles/graphql/queries/UserMembersList.ts new file mode 100644 index 00000000..e50bbda4 --- /dev/null +++ b/packages/components/modules/profiles/graphql/queries/UserMembersList.ts @@ -0,0 +1,40 @@ +import { graphql } from 'react-relay' + +export const UserMembersListPaginationQuery = graphql` + query UserMembersListPaginationQuery( + $count: Int = 10 + $cursor: String + $orderBy: String + $profileId: ID! + ) { + profile(id: $profileId) { + pk + ...UserMembersListFragment @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } +` + +export const UserMembersListFragment = graphql` + fragment UserMembersListFragment on Profile + @refetchable(queryName: "userMembersListPaginationRefetchable") + @argumentDefinitions( + count: { type: "Int", defaultValue: 10 } + cursor: { type: "String" } + orderBy: { type: "String" } + ) { + ...ProfileItemFragment + members(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "UserMembersFragment_members", filters: ["orderBy"]) { + totalCount + edges { + node { + ...MemberItemFragment + } + } + pageInfo { + endCursor + hasNextPage + } + } + } +` diff --git a/packages/components/modules/profiles/index.ts b/packages/components/modules/profiles/index.ts index b873195a..58da19e3 100644 --- a/packages/components/modules/profiles/index.ts +++ b/packages/components/modules/profiles/index.ts @@ -4,3 +4,4 @@ export { default as CurrentProfileProvider } from './context/CurrentProfileProvi // Components export * from './ProfilePopover' +export { default as Members } from './Members' diff --git a/packages/components/package.json b/packages/components/package.json index 11194558..1b8cf470 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,7 +1,7 @@ { "name": "@baseapp-frontend/components", "description": "BaseApp components modules such as comments, notifications, messages, and more.", - "version": "0.0.22", + "version": "0.0.23", "main": "./index.ts", "types": "dist/index.d.ts", "sideEffects": false, diff --git a/packages/components/schema.graphql b/packages/components/schema.graphql index 67b3abdb..1a094599 100644 --- a/packages/components/schema.graphql +++ b/packages/components/schema.graphql @@ -4,21 +4,6 @@ directive @specifiedBy( url: String! ) on SCALAR -"""An enumeration.""" -enum BaseappNotificationsNotificationLevelChoices { - """success""" - SUCCESS - - """info""" - INFO - - """warning""" - WARNING - - """error""" - ERROR -} - type Block implements Node { """The ID of the object""" id: ID! @@ -604,7 +589,7 @@ interface Node { type Notification implements Node { """The ID of the object""" id: ID! - level: BaseappNotificationsNotificationLevelChoices! + level: NotificationsNotificationLevelChoices! recipient: User! unread: Boolean! actorObjectId: String! @@ -706,7 +691,7 @@ interface NotificationsInterface { """The ID of the object""" id: ID! notificationsUnreadCount: Int - notifications(offset: Int, before: String, after: String, first: Int, last: Int, level: BaseappNotificationsNotificationLevelChoices, unread: Boolean, verbs: String): NotificationConnection + notifications(offset: Int, before: String, after: String, first: Int, last: Int, level: NotificationsNotificationLevelChoices, unread: Boolean, verbs: String): NotificationConnection notificationSettings(offset: Int, before: String, after: String, first: Int, last: Int): NotificationSettingConnection isNotificationSettingActive(verb: String!, channel: NotificationChannelTypes!): Boolean } @@ -741,6 +726,21 @@ type NotificationsMarkAsReadPayload { clientMutationId: String } +"""An enumeration.""" +enum NotificationsNotificationLevelChoices { + """success""" + SUCCESS + + """info""" + INFO + + """warning""" + WARNING + + """error""" + ERROR +} + type OnCommentChange { createdComment: CommentEdge updatedComment: Comment @@ -916,7 +916,17 @@ type Profile implements Node & PermissionsInterface & PageInterface & FollowsInt reactions(offset: Int, before: String, after: String, first: Int, last: Int, id: ID): ReactionConnection! ratings(offset: Int, before: String, after: String, first: Int, last: Int): RateConnection! user: User - members(offset: Int, before: String, after: String, first: Int, last: Int, role: ProfileRoles): ProfileUserRoleConnection + members( + offset: Int + before: String + after: String + first: Int + last: Int + role: ProfileRoles + + """Ordering""" + orderBy: String + ): ProfileUserRoleConnection chatroomparticipantSet(offset: Int, before: String, after: String, first: Int, last: Int, profile_TargetContentType: ID): ChatRoomParticipantConnection! messageSet(offset: Int, before: String, after: String, first: Int, last: Int, verb: Verbs): MessageConnection! following(offset: Int, before: String, after: String, first: Int, last: Int, targetIsFollowingBack: Boolean): FollowConnection @@ -1390,7 +1400,7 @@ type User implements Node & PermissionsInterface & NotificationsInterface & Page """ hasPerm(perm: String!): Boolean notificationsUnreadCount: Int - notifications(offset: Int, before: String, after: String, first: Int, last: Int, level: BaseappNotificationsNotificationLevelChoices, unread: Boolean, verbs: String): NotificationConnection + notifications(offset: Int, before: String, after: String, first: Int, last: Int, level: NotificationsNotificationLevelChoices, unread: Boolean, verbs: String): NotificationConnection notificationSettings(offset: Int, before: String, after: String, first: Int, last: Int): NotificationSettingConnection isNotificationSettingActive(verb: String!, channel: NotificationChannelTypes!): Boolean urlPath: URLPath diff --git a/packages/design-system/components/inputs/TextField/index.tsx b/packages/design-system/components/inputs/TextField/index.tsx index b6611663..9292fc97 100644 --- a/packages/design-system/components/inputs/TextField/index.tsx +++ b/packages/design-system/components/inputs/TextField/index.tsx @@ -4,14 +4,12 @@ import { FC } from 'react' import { withController } from '@baseapp-frontend/utils' -import { useTheme } from '@emotion/react' import { TextField as MUITextField, Theme, useMediaQuery } from '@mui/material' import { PureTextFieldProps, TextFieldProps } from './types' const TextField: FC = ({ isResponsive = true, ...props }) => { - const theme = useTheme() as Theme - const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')) return }