Skip to content

Commit f3b1032

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

24 files changed

Lines changed: 1642 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: 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;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React, { useMemo } from 'react';
2+
import { LocationOn } from '@openedx/paragon/icons';
3+
import { useScopes } from '@src/authz-module/data/hooks';
4+
import { MultipleChoiceFilterProps } from './types';
5+
import MultipleChoiceFilter from './MultipleChoiceFilter';
6+
7+
type ScopesFilterProps = Omit<MultipleChoiceFilterProps, 'filterChoices' | 'isSearchable' | 'onSearchChange'>;
8+
9+
const ScopesFilter = ({
10+
filterButtonText, filterValue, setFilter, disabled,
11+
}: ScopesFilterProps) => {
12+
const [searchValue, setSearchValue] = React.useState<string | undefined>(undefined);
13+
const { data: scopesData = { scopes: [] } } = useScopes(searchValue);
14+
15+
const filterChoices = useMemo(() => scopesData.scopes.map((scope) => ({
16+
displayName: scope.name,
17+
value: scope.key,
18+
groupName: scope.organization.name,
19+
})), [scopesData]);
20+
21+
const handleSearchChange = (value: string) => {
22+
setSearchValue(value);
23+
};
24+
25+
return (
26+
<MultipleChoiceFilter
27+
filterButtonText={filterButtonText}
28+
filterChoices={filterChoices}
29+
filterValue={filterValue}
30+
setFilter={setFilter}
31+
isSearchable
32+
isGrouped
33+
onSearchChange={handleSearchChange}
34+
iconSrc={LocationOn}
35+
disabled={disabled}
36+
/>
37+
);
38+
};
39+
40+
export default ScopesFilter;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
Form,
3+
Icon,
4+
} from '@openedx/paragon';
5+
import { Search } from '@openedx/paragon/icons';
6+
7+
interface SearchFilterProps {
8+
filterValue: string;
9+
setFilter: (value: string) => void;
10+
placeholder: string;
11+
}
12+
13+
const SearchFilter = ({
14+
filterValue, setFilter, placeholder,
15+
}: SearchFilterProps) => (
16+
<Form.Group className="m-0">
17+
<Form.Control
18+
className="mw-xs mr-0"
19+
trailingElement={<Icon src={Search} />}
20+
value={filterValue || ''}
21+
type="text"
22+
onChange={e => {
23+
setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely
24+
}}
25+
placeholder={placeholder}
26+
/>
27+
</Form.Group>
28+
);
29+
30+
export default SearchFilter;

0 commit comments

Comments
 (0)