participantList.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {AnimatePresence, motion} from 'framer-motion';
  4. import Avatar from 'sentry/components/avatar';
  5. import TeamAvatar from 'sentry/components/avatar/teamAvatar';
  6. import {Button} from 'sentry/components/button';
  7. import {IconChevron} from 'sentry/icons';
  8. import {t, tn} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {Team} from 'sentry/types/organization';
  11. import type {User} from 'sentry/types/user';
  12. interface ParticipantScrollboxProps {
  13. teams: Team[];
  14. users: User[];
  15. }
  16. function ParticipantScrollbox({users, teams}: ParticipantScrollboxProps) {
  17. if (!users.length && !teams.length) {
  18. return null;
  19. }
  20. const showHeaders = users.length > 0 && teams.length > 0;
  21. return (
  22. <ParticipantListWrapper>
  23. {showHeaders && <ListTitle>{t('Teams (%s)', teams.length)}</ListTitle>}
  24. {teams.map(team => (
  25. <UserRow key={team.id}>
  26. <TeamAvatar team={team} size={28} />
  27. <div>
  28. {`#${team.slug}`}
  29. <SubText>{tn('%s member', '%s members', team.memberCount)}</SubText>
  30. </div>
  31. </UserRow>
  32. ))}
  33. {showHeaders && <ListTitle>{t('Individuals (%s)', users.length)}</ListTitle>}
  34. {users.map(user => (
  35. <UserRow key={user.id}>
  36. <Avatar user={user} size={28} />
  37. <div>
  38. {user.name}
  39. <SubText>{user.email}</SubText>
  40. </div>
  41. </UserRow>
  42. ))}
  43. </ParticipantListWrapper>
  44. );
  45. }
  46. interface ParticipantListProps {
  47. children: React.ReactNode;
  48. description: string;
  49. users: User[];
  50. teams?: Team[];
  51. }
  52. export function ParticipantList({teams = [], users, children}: ParticipantListProps) {
  53. const [isExpanded, setIsExpanded] = useState(false);
  54. return (
  55. <Fragment>
  56. <ParticipantWrapper onClick={() => setIsExpanded(!isExpanded)} role="button">
  57. {children}
  58. <Button
  59. borderless
  60. size="zero"
  61. icon={
  62. <IconChevron
  63. direction={isExpanded ? 'up' : 'down'}
  64. size="xs"
  65. color="gray300"
  66. />
  67. }
  68. aria-label={t('%s Participants', isExpanded ? t('Collapse') : t('Expand'))}
  69. onClick={() => setIsExpanded(!isExpanded)}
  70. />
  71. </ParticipantWrapper>
  72. <AnimatePresence>
  73. {isExpanded && (
  74. <motion.div
  75. variants={{
  76. open: {height: '100%', opacity: 1, overflow: 'initial'},
  77. closed: {height: '0', opacity: 0, overflow: 'hidden'},
  78. }}
  79. initial="closed"
  80. animate="open"
  81. exit="closed"
  82. >
  83. <ParticipantScrollbox users={users} teams={teams} />
  84. </motion.div>
  85. )}
  86. </AnimatePresence>
  87. </Fragment>
  88. );
  89. }
  90. const ParticipantWrapper = styled('div')`
  91. display: flex;
  92. align-items: center;
  93. justify-content: space-between;
  94. cursor: pointer;
  95. padding-bottom: ${space(1)};
  96. & > span {
  97. cursor: pointer;
  98. }
  99. `;
  100. const ParticipantListWrapper = styled('div')`
  101. max-height: 325px;
  102. overflow-y: auto;
  103. border: 1px solid ${p => p.theme.border};
  104. border-radius: ${p => p.theme.borderRadius};
  105. & > div:not(:last-child) {
  106. border-bottom: 1px solid ${p => p.theme.border};
  107. }
  108. & > div:first-child {
  109. border-top-left-radius: ${p => p.theme.borderRadius};
  110. border-top-right-radius: ${p => p.theme.borderRadius};
  111. }
  112. & > div:last-child {
  113. border-bottom-left-radius: ${p => p.theme.borderRadius};
  114. border-bottom-right-radius: ${p => p.theme.borderRadius};
  115. }
  116. `;
  117. const ListTitle = styled('div')`
  118. display: flex;
  119. align-items: center;
  120. padding: ${space(1)} ${space(1.5)};
  121. background-color: ${p => p.theme.backgroundSecondary};
  122. color: ${p => p.theme.gray300};
  123. text-transform: uppercase;
  124. font-weight: ${p => p.theme.fontWeightBold};
  125. font-size: ${p => p.theme.fontSizeSmall};
  126. `;
  127. const UserRow = styled('div')`
  128. display: flex;
  129. align-items: center;
  130. padding: ${space(1)} ${space(1.5)};
  131. gap: ${space(1)};
  132. line-height: 1.2;
  133. font-size: ${p => p.theme.fontSizeSmall};
  134. `;
  135. const SubText = styled('div')`
  136. color: ${p => p.theme.subText};
  137. font-size: ${p => p.theme.fontSizeExtraSmall};
  138. `;