-
-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(organizations): members table TASK-980 (#5261)
Add MembersRoute component that renders things at `#/account/organization/members` (a header and a table of organization members). Add `membersQuery` that defines `OrganizationMember` TS interface and adds way for getting organization members. Change minimum column size in UniversalTable props to 60 (smaller then previously).
- Loading branch information
1 parent
79e33e8
commit 1386a15
Showing
8 changed files
with
221 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
// Libraries | ||
import React from 'react'; | ||
|
||
// Partial components | ||
import PaginatedQueryUniversalTable from 'js/universalTable/paginatedQueryUniversalTable.component'; | ||
import LoadingSpinner from 'js/components/common/loadingSpinner'; | ||
import Avatar from 'js/components/common/avatar'; | ||
import Badge from 'jsapp/js/components/common/badge'; | ||
|
||
// Stores, hooks and utilities | ||
import {formatTime} from 'js/utils'; | ||
import {useOrganizationQuery} from './organizationQuery'; | ||
import useOrganizationMembersQuery from './membersQuery'; | ||
|
||
// Constants and types | ||
import type {OrganizationMember} from './membersQuery'; | ||
|
||
// Styles | ||
import styles from './membersRoute.module.scss'; | ||
|
||
export default function MembersRoute() { | ||
const orgQuery = useOrganizationQuery(); | ||
|
||
if (!orgQuery.data?.id) { | ||
return ( | ||
<LoadingSpinner /> | ||
); | ||
} | ||
|
||
return ( | ||
<div className={styles.membersRouteRoot}> | ||
<header className={styles.header}> | ||
<h2 className={styles.headerText}>{t('Members')}</h2> | ||
</header> | ||
|
||
<PaginatedQueryUniversalTable<OrganizationMember> | ||
queryHook={useOrganizationMembersQuery} | ||
columns={[ | ||
{ | ||
key: 'user__extra_details__name', | ||
label: t('Name'), | ||
cellFormatter: (member: OrganizationMember) => ( | ||
<Avatar | ||
size='m' | ||
username={member.user__username} | ||
isUsernameVisible | ||
email={member.user__email} | ||
// We pass `undefined` for the case it's an empty string | ||
fullName={member.user__extra_details__name || undefined} | ||
/> | ||
), | ||
size: 360, | ||
}, | ||
{ | ||
key: 'invite', | ||
label: t('Status'), | ||
size: 120, | ||
cellFormatter: (member: OrganizationMember) => { | ||
if (member.invite?.status) { | ||
return member.invite.status; | ||
} else { | ||
return <Badge color='light-green' size='s' label={t('Active')} />; | ||
} | ||
return null; | ||
}, | ||
}, | ||
{ | ||
key: 'date_joined', | ||
label: t('Date added'), | ||
size: 140, | ||
cellFormatter: (member: OrganizationMember) => formatTime(member.date_joined), | ||
}, | ||
{ | ||
key: 'role', | ||
label: t('Role'), | ||
size: 120, | ||
}, | ||
{ | ||
key: 'user__has_mfa_enabled', | ||
label: t('2FA'), | ||
size: 90, | ||
cellFormatter: (member: OrganizationMember) => { | ||
if (member.user__has_mfa_enabled) { | ||
return <Badge size='s' color='light-blue' icon='check' />; | ||
} | ||
return <Badge size='s' color='light-storm' icon='minus' />; | ||
}, | ||
}, | ||
{ | ||
// We use `url` here, but the cell would contain interactive UI | ||
// element | ||
key: 'url', | ||
label: '', | ||
size: 64, | ||
// TODO: this will be added soon | ||
cellFormatter: () => (' '), | ||
}, | ||
]} | ||
/> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import {keepPreviousData, useQuery} from '@tanstack/react-query'; | ||
import {endpoints} from 'js/api.endpoints'; | ||
import type {PaginatedResponse} from 'js/dataInterface'; | ||
import {fetchGet} from 'js/api'; | ||
import {QueryKeys} from 'js/query/queryKeys'; | ||
import {useOrganizationQuery, type OrganizationUserRole} from './organizationQuery'; | ||
|
||
export interface OrganizationMember { | ||
/** | ||
* The url to the member within the organization | ||
* `/api/v2/organizations/<organization_uid>/members/<username>/` | ||
*/ | ||
url: string; | ||
/** `/api/v2/users/<username>/` */ | ||
user: string; | ||
user__username: string; | ||
/** can be an empty string in some edge cases */ | ||
user__email: string | ''; | ||
/** can be an empty string in some edge cases */ | ||
user__extra_details__name: string | ''; | ||
role: OrganizationUserRole; | ||
user__has_mfa_enabled: boolean; | ||
user__is_active: boolean; | ||
/** yyyy-mm-dd HH:MM:SS */ | ||
date_joined: string; | ||
invite?: { | ||
/** '/api/v2/organizations/<organization_uid>/invites/<invite_uid>/' */ | ||
url: string; | ||
/** yyyy-mm-dd HH:MM:SS */ | ||
date_created: string; | ||
/** yyyy-mm-dd HH:MM:SS */ | ||
date_modified: string; | ||
status: 'sent' | 'accepted' | 'expired' | 'declined'; | ||
}; | ||
} | ||
|
||
/** | ||
* Fetches paginated list of members for given organization. | ||
* This is mainly needed for `useOrganizationMembersQuery`, so you most probably | ||
* would use it through that hook rather than directly. | ||
*/ | ||
async function getOrganizationMembers( | ||
limit: number, | ||
offset: number, | ||
orgId: string | ||
) { | ||
const params = new URLSearchParams({ | ||
limit: limit.toString(), | ||
offset: offset.toString(), | ||
}); | ||
|
||
const apiUrl = endpoints.ORGANIZATION_MEMBERS_URL.replace(':organization_id', orgId); | ||
|
||
return fetchGet<PaginatedResponse<OrganizationMember>>( | ||
apiUrl + '?' + params, | ||
{ | ||
errorMessageDisplay: t('There was an error getting the list.'), | ||
} | ||
); | ||
} | ||
|
||
/** | ||
* A hook that gives you paginated list of organization members. Uses | ||
* `useOrganizationQuery` to get the id. | ||
*/ | ||
export default function useOrganizationMembersQuery( | ||
itemLimit: number, | ||
pageOffset: number | ||
) { | ||
const orgQuery = useOrganizationQuery(); | ||
const orgId = orgQuery.data?.id; | ||
|
||
return useQuery({ | ||
queryKey: [QueryKeys.organizationMembers, itemLimit, pageOffset, orgId], | ||
// `orgId!` because it's ensured to be there in `enabled` property :ok: | ||
queryFn: () => getOrganizationMembers(itemLimit, pageOffset, orgId!), | ||
placeholderData: keepPreviousData, | ||
enabled: !!orgId, | ||
// We might want to improve this in future, for now let's not retry | ||
retry: false, | ||
// The `refetchOnWindowFocus` option is `true` by default, I'm setting it | ||
// here so we don't forget about it. | ||
refetchOnWindowFocus: true, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
@use 'scss/colors'; | ||
@use 'scss/breakpoints'; | ||
|
||
.membersRouteRoot { | ||
padding: 20px; | ||
overflow-y: auto; | ||
height: 100%; | ||
} | ||
|
||
.header { | ||
margin-bottom: 20px; | ||
} | ||
|
||
h2.headerText { | ||
color: colors.$kobo-storm; | ||
text-transform: uppercase; | ||
font-size: 18px; | ||
font-weight: 700; | ||
margin: 0; | ||
} | ||
|
||
@include breakpoints.breakpoint(mediumAndUp) { | ||
.membersRouteRoot { | ||
padding: 50px; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters