Skip to content

Commit 99d89d7

Browse files
feat: creating the team members tab with the new ui
1 parent 8c76f6b commit 99d89d7

24 files changed

Lines changed: 1660 additions & 23 deletions

src/authz-module/authz-home/index.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useIntl } from '@edx/frontend-platform/i18n';
22
import { Tab, Tabs } from '@openedx/paragon';
3-
import { useLocation } from 'react-router-dom';
3+
import { useLocation, useSearchParams } from 'react-router-dom';
4+
import TeamMembersTable from 'authz-module/team-members/TeamMembersTable';
5+
import AddRoleButton from '@src/authz-module/components/AddRoleButton';
46
import RolesPermissions from '../roles-permissions/RolesPermissions';
57
import AuthZLayout from '../components/AuthZLayout';
68

@@ -9,6 +11,8 @@ import messages from '../libraries-manager/messages';
911
const AuthzHome = () => {
1012
const { hash } = useLocation();
1113
const intl = useIntl();
14+
const [searchParams] = useSearchParams();
15+
const presetScope = searchParams.get('scope') || undefined;
1216

1317
const rootBreadcrumb = intl.formatMessage(messages['library.authz.breadcrumb.root']) || '';
1418
const pageTitle = intl.formatMessage(messages['library.authz.manage.page.title']);
@@ -22,11 +26,7 @@ const AuthzHome = () => {
2226
pageTitle={pageTitle}
2327
pageSubtitle=""
2428
actions={
25-
[]
26-
// this needs to be enable again once is refactored to be used outside of library context
27-
// [
28-
// <AddNewTeamMemberTrigger libraryId="" key="add-new-member" />,
29-
// ]
29+
[<AddRoleButton key="add-role-button" />]
3030
}
3131
>
3232
<Tabs
@@ -35,8 +35,7 @@ const AuthzHome = () => {
3535
className="bg-light-100 px-5"
3636
>
3737
<Tab eventKey="team" title={intl.formatMessage(messages['library.authz.tabs.team'])} className="p-5">
38-
{/* TODO: once TeamTable is refactored we can call it here. For now, this tab will be empty. */}
39-
{/* <TeamTable /> */}
38+
<TeamMembersTable presetScope={presetScope} />
4039
</Tab>
4140
<Tab id="libraries-permissions-roles-tab" eventKey="permissionsRoles" title={intl.formatMessage(messages['library.authz.tabs.permissionsRoles'])}>
4241
<RolesPermissions />
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { Button } from '@openedx/paragon';
4+
import { Plus } from '@openedx/paragon/icons';
5+
6+
import baseMessages from '@src/authz-module/messages';
7+
import { useNavigate } from 'react-router-dom';
8+
9+
interface AddRoleButtonProps {
10+
presetUsername?: string;
11+
}
12+
13+
const AddRoleButton = ({ presetUsername }: AddRoleButtonProps) => {
14+
const intl = useIntl();
15+
const navigate = useNavigate();
16+
17+
const handleClick = () => {
18+
const assignRolePath = `/authz/assign-role${presetUsername ? `?username=${presetUsername}` : ''}`;
19+
navigate(assignRolePath);
20+
};
21+
22+
return (
23+
<Button
24+
iconBefore={Plus}
25+
onClick={handleClick}
26+
>
27+
{intl.formatMessage(baseMessages['authz.management.assign.role.title'])}
28+
</Button>
29+
);
30+
};
31+
32+
export default AddRoleButton;

src/authz-module/components/AuthZLayout.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ interface AuthZLayoutProps extends AuthZTitleProps {
1414
const AuthZLayout = ({ children, context, ...props }: AuthZLayoutProps) => (
1515
<>
1616
<StudioHeader
17-
number={context.id}
18-
org={context.org}
19-
title={context.title}
17+
number={context?.id || null}
18+
org={context?.org || null}
19+
title={context?.title || null}
20+
isHiddenMainMenu
2021
/>
2122
<AuthZTitle {...props} />
2223
{children}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import { Icon, IconButton } from '@openedx/paragon';
3+
import { AppContext } from '@edx/frontend-platform/react';
4+
import {
5+
RemoveRedEye, Language, School, LibraryBooks,
6+
} from '@openedx/paragon/icons';
7+
import { TableCellValue, TeamMember, AppContextType } from '@src/types';
8+
import { useNavigate } from 'react-router-dom';
9+
import { useContext } from 'react';
10+
import messages from './messages';
11+
12+
type CellProps = TableCellValue<TeamMember>;
13+
type ExtendedCellProps = CellProps & {
14+
value: string;
15+
cell: {
16+
getCellProps: (props?: Record<string, string>) => Record<string, string>;
17+
};
18+
};
19+
20+
const SCOPE_ICONS = {
21+
COURSE: School,
22+
LIBRARY: LibraryBooks,
23+
GLOBAL: Language,
24+
};
25+
26+
const NameCell = ({ row }: CellProps) => {
27+
const intl = useIntl();
28+
const { authenticatedUser } = useContext(AppContext) as AppContextType;
29+
const username = authenticatedUser?.username;
30+
31+
if (row.original.username === username) {
32+
return (
33+
<span>
34+
{row.original.fullName}
35+
<span className="text-gray-500">{intl.formatMessage(messages['authz.table.username.current'])}</span>
36+
</span>
37+
);
38+
}
39+
return row.original.fullName;
40+
};
41+
42+
const ActionCell = ({ row }: CellProps) => {
43+
const { formatMessage } = useIntl();
44+
const navigate = useNavigate();
45+
const viewPath = `/authz/user/${row.original.username}`;
46+
return (
47+
<IconButton
48+
src={RemoveRedEye}
49+
alt={formatMessage(messages['authz.table.column.actions.view.title'])}
50+
size="sm"
51+
onClick={() => navigate(viewPath)}
52+
/>
53+
);
54+
};
55+
56+
const ScopeCell = ({ row }: CellProps) => {
57+
const { scope } = row.original;
58+
const iconSrc = SCOPE_ICONS[scope.type];
59+
return (
60+
<span className="d-flex align-items-center">
61+
{iconSrc && <Icon color="primary" src={iconSrc} className="mr-2" size="xs" />}
62+
{scope.resource}
63+
</span>
64+
);
65+
};
66+
67+
const RoleCell = ({ value, cell }: ExtendedCellProps) => (
68+
<td {...cell.getCellProps({ 'data-role': value })}>
69+
{value}
70+
</td>
71+
);
72+
73+
export {
74+
NameCell, ActionCell, ScopeCell, RoleCell,
75+
};
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import {
2+
Dropdown, Form, Icon, Stack,
3+
} from '@openedx/paragon';
4+
import { useIntl } from '@edx/frontend-platform/i18n';
5+
import { FilterList, Search } from '@openedx/paragon/icons';
6+
import { useState } from 'react';
7+
import messages from '../messages';
8+
import { MultipleChoiceFilterProps } from './types';
9+
10+
const MultipleChoiceFilter = ({
11+
filterButtonText,
12+
filterChoices,
13+
filterValue,
14+
setFilter,
15+
isGrouped = false,
16+
isSearchable = false,
17+
onSearchChange,
18+
iconSrc,
19+
disabled = false,
20+
}: MultipleChoiceFilterProps) => {
21+
const [searchValue, setSearchValue] = useState<string | undefined>(undefined);
22+
const { formatMessage } = useIntl();
23+
const checkedBoxes = filterValue || [];
24+
const handleClickCheckbox = (value) => {
25+
const newValue = {
26+
groupName: filterButtonText?.toLocaleLowerCase() || '',
27+
value,
28+
displayName: value,
29+
};
30+
if (checkedBoxes.includes(value)) {
31+
const newCheckedBoxes = checkedBoxes.filter((val) => val !== value);
32+
return setFilter(newCheckedBoxes, newValue);
33+
}
34+
checkedBoxes.push(value);
35+
return setFilter(checkedBoxes, newValue);
36+
};
37+
38+
const getGroupedChoices = () => {
39+
const groupedFilterChoices = filterChoices.reduce((groups, choice) => {
40+
const groupName = choice.groupName || 'Ungrouped';
41+
const icon = choice.groupIcon || undefined;
42+
if (!groups.has(groupName)) {
43+
groups.set(groupName, { groupName, options: [], icon });
44+
}
45+
groups.get(groupName)!.options.push({
46+
displayName: choice.displayName,
47+
value: choice.value,
48+
});
49+
return groups;
50+
}, new Map<string, { groupName: string; options: Array<{ displayName: string; value: string }>; icon?: any }>());
51+
return Array.from(groupedFilterChoices.values());
52+
};
53+
return (
54+
<Dropdown className="no-caret-dropdown filters">
55+
<Dropdown.Toggle variant={checkedBoxes.length > 0 ? 'primary' : 'outline-primary'}>
56+
<Stack direction="horizontal" gap={2}>
57+
{iconSrc && <Icon color="primary" src={iconSrc} />}
58+
{filterButtonText}
59+
{checkedBoxes.length > 0 && ` (${checkedBoxes.length})`}
60+
<Icon color="primary" src={FilterList} />
61+
</Stack>
62+
</Dropdown.Toggle>
63+
64+
<Dropdown.Menu>
65+
{isSearchable && (
66+
<Form.Control
67+
className="m-1"
68+
type="text"
69+
trailingElement={<Icon src={Search} />}
70+
placeholder={formatMessage(messages['authz.table.controlbar.search'])}
71+
onChange={(e) => {
72+
setSearchValue(e.target.value);
73+
onSearchChange?.(e.target.value);
74+
}}
75+
value={searchValue}
76+
/>
77+
)}
78+
<Form.CheckboxSet
79+
className="pgn__dropdown-filter-checkbox-group"
80+
name={filterButtonText}
81+
aria-label={filterButtonText}
82+
value={checkedBoxes}
83+
>
84+
{/** TODO: Change for actual values */}
85+
<span className="small text-info-700 mt-2">{formatMessage(messages['authz.table.controlbar.filters.items.showing'], { current: filterChoices.length, total: filterChoices.length })}</span>
86+
{!isGrouped ? filterChoices.map(({
87+
displayName, value,
88+
}) => (
89+
<Form.Checkbox
90+
className="m-2"
91+
key={displayName}
92+
checked={checkedBoxes.includes(value)}
93+
value={value}
94+
onChange={() => handleClickCheckbox(value)}
95+
aria-label={displayName}
96+
disabled={checkedBoxes.includes(value) ? false : disabled}
97+
>
98+
<span className="small">{displayName}</span>
99+
</Form.Checkbox>
100+
))
101+
: getGroupedChoices().map(({ groupName, icon, options }) => (
102+
<div key={groupName}>
103+
<div className="pgn__dropdown-filter-group-name text-info-700 d-flex align-items-center small m-2 ml-0">
104+
{icon && <Icon color="primary" src={icon} className="mr-2" size="xs" />}
105+
<span>{groupName}</span>
106+
</div>
107+
{options.map(({ displayName, value }) => (
108+
<Form.Checkbox
109+
className="m-2"
110+
key={displayName}
111+
value={value}
112+
onChange={() => handleClickCheckbox(value)}
113+
disabled={checkedBoxes.includes(value) ? false : disabled}
114+
aria-label={displayName}
115+
>
116+
<span className="small">{displayName}</span>
117+
</Form.Checkbox>
118+
))}
119+
</div>
120+
))}
121+
</Form.CheckboxSet>
122+
</Dropdown.Menu>
123+
</Dropdown>
124+
);
125+
};
126+
127+
export default MultipleChoiceFilter;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { useMemo } from 'react';
2+
import { Business } from '@openedx/paragon/icons';
3+
import { useOrgs } from '@src/authz-module/data/hooks';
4+
import { MultipleChoiceFilterProps } from './types';
5+
import MultipleChoiceFilter from './MultipleChoiceFilter';
6+
7+
type OrgFilterProps = Omit<MultipleChoiceFilterProps, 'filterChoices' | 'isSearchable' | 'onSearchChange'>;
8+
9+
const OrgFilter = ({
10+
filterButtonText, filterValue, setFilter, disabled,
11+
}: OrgFilterProps) => {
12+
const [searchValue, setSearchValue] = React.useState<string | undefined>(undefined);
13+
const { data: orgsData = { orgs: [] } } = useOrgs(searchValue);
14+
15+
const filterChoices = useMemo(() => orgsData.orgs.map((org) => ({
16+
displayName: org.name,
17+
value: org.id,
18+
})), [orgsData]);
19+
20+
const handleSearchChange = (value: string) => {
21+
setSearchValue(value);
22+
};
23+
24+
return (
25+
<MultipleChoiceFilter
26+
filterButtonText={filterButtonText}
27+
filterChoices={filterChoices}
28+
filterValue={filterValue}
29+
setFilter={setFilter}
30+
isSearchable
31+
onSearchChange={handleSearchChange}
32+
iconSrc={Business}
33+
disabled={disabled}
34+
/>
35+
);
36+
};
37+
38+
export default OrgFilter;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useMemo } from 'react';
2+
import {
3+
Person, Language, School, LibraryBooks,
4+
} from '@openedx/paragon/icons';
5+
import MultipleChoiceFilter from './MultipleChoiceFilter';
6+
import { MultipleChoiceFilterProps } from './types';
7+
8+
type RolesFilterProps = Omit<MultipleChoiceFilterProps, 'filterChoices' | 'isSearchable' | 'onSearchChange'>;
9+
10+
const RolesFilter = ({
11+
filterButtonText, filterValue, setFilter, disabled,
12+
}: RolesFilterProps) => {
13+
// TODO: use a constant
14+
const filterChoices = useMemo(() => [
15+
{
16+
groupName: 'Global', groupIcon: Language, displayName: 'Super Admin', value: 'Super Admin',
17+
},
18+
{
19+
groupName: 'Global', groupIcon: Language, displayName: 'Global Staff', value: 'Global Staff',
20+
},
21+
22+
{
23+
groupName: 'Course', groupIcon: School, displayName: 'Course Admin', value: 'Course Admin',
24+
},
25+
{
26+
groupName: 'Course', groupIcon: School, displayName: 'Course Staff', value: 'Course Staff',
27+
},
28+
{
29+
groupName: 'Course', groupIcon: School, displayName: 'Course Editor', value: 'Course Editor',
30+
},
31+
{
32+
groupName: 'Course', groupIcon: School, displayName: 'Course Auditor', value: 'Course Auditor',
33+
},
34+
35+
{
36+
groupName: 'Library', groupIcon: LibraryBooks, displayName: 'Library Admin', value: 'Library Admin',
37+
},
38+
{
39+
groupName: 'Library', groupIcon: LibraryBooks, displayName: 'Library Author', value: 'Library Author',
40+
},
41+
{
42+
groupName: 'Library', groupIcon: LibraryBooks, displayName: 'Library Collaborator', value: 'Library Collaborator',
43+
},
44+
{
45+
groupName: 'Library', groupIcon: LibraryBooks, displayName: 'Library User', value: 'Library User',
46+
},
47+
], []);
48+
return (
49+
<MultipleChoiceFilter
50+
filterButtonText={filterButtonText}
51+
filterChoices={filterChoices}
52+
filterValue={filterValue}
53+
setFilter={setFilter}
54+
isGrouped
55+
iconSrc={Person}
56+
disabled={disabled}
57+
/>
58+
);
59+
};
60+
61+
export default RolesFilter;

0 commit comments

Comments
 (0)