Skip to content

Commit 74492fc

Browse files
Merge pull request #5098 from OneCommunityGlobal/Diya_UserStateFeature
Diya 🔥 feat: Enhanced existing User State Feature
2 parents cc39f6f + 6810ccb commit 74492fc

14 files changed

Lines changed: 1488 additions & 527 deletions

File tree

src/components/PermissionsManagement/__tests__/RolePermissions.test.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ describe('RolePermissions component', () => {
206206
const history = createMemoryHistory();
207207
renderComponent(store, history, roleName, roleId);
208208

209-
fireEvent.click(screen.getByTestId('edit-role-icon'));
209+
const editIcon = await screen.findByTestId('edit-role-icon');
210+
fireEvent.click(editIcon);
210211

211212
const dialog = screen.getByRole('dialog');
212213

src/components/TeamMemberTasks/TeamMemberTask.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '@fortawesome/free-solid-svg-icons';
99
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
1010
import PropTypes from 'prop-types';
11-
import React, { useRef, useState } from 'react';
11+
import React, { useRef, useState, useEffect } from 'react';
1212
import { Modal, ModalBody, ModalFooter, ModalHeader, Progress, Table } from 'reactstrap';
1313
import CopyToClipboard from '~/components/common/Clipboard/CopyToClipboard';
1414
import UserStateDisplay from '../UserState/UserStateDisplay';
@@ -65,6 +65,9 @@ const TeamMemberTask = React.memo(
6565
const manager = 'Manager';
6666
const adm = 'Administrator';
6767
const owner = 'Owner';
68+
const isOwnerOrAdmin = ['Owner', 'Administrator'].includes(userRole);
69+
70+
useEffect(() => {}, [userStateSelection]);
6871

6972
const handleDashboardAccess = () => {
7073
// null checks
@@ -528,7 +531,9 @@ const TeamMemberTask = React.memo(
528531
</div>
529532
<UserStateDisplay
530533
userId={user.personId}
534+
userName={user.name}
531535
canEdit={displayUser?.email === 'jae@onecommunityglobal.org'}
536+
canManage={isOwnerOrAdmin}
532537
catalog={userStateCatalog}
533538
onCatalogChange={onCatalogChange}
534539
initialSelected={userStateSelection}

src/components/TeamMemberTasks/TeamMemberTasks.jsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const TeamMemberTasks = React.memo(props => {
6767

6868
const [userStateCatalog, setUserStateCatalog] = useState([]);
6969
const [userStateSelections, setUserStateSelections] = useState({});
70+
const [selectionsLoaded, setSelectionsLoaded] = useState(false);
7071

7172
useEffect(() => {
7273
axios
@@ -77,15 +78,19 @@ const TeamMemberTasks = React.memo(props => {
7778

7879
// Fetch all selections in ONE call once teamList is ready
7980
useEffect(() => {
80-
if (teamList.length === 0) return;
81-
const userIds = teamList.map(u => u.personId);
82-
const result = axios.post(ENDPOINTS.USER_STATE_SELECTIONS_BATCH, { userIds });
83-
if (result && typeof result.then === 'function') {
84-
result
85-
.then(res => setUserStateSelections(res.data.selections || {}))
86-
.catch(() => setUserStateSelections({}));
87-
}
88-
}, [teamList]);
81+
if (usersWithTasks.length === 0) return;
82+
const userIds = usersWithTasks.map(u => u.personId);
83+
axios
84+
.post(ENDPOINTS.USER_STATE_SELECTIONS_BATCH, { userIds })
85+
.then(res => {
86+
setUserStateSelections(res.data.selections || {});
87+
setSelectionsLoaded(true);
88+
})
89+
.catch(() => {
90+
setUserStateSelections({});
91+
setSelectionsLoaded(true);
92+
});
93+
}, [usersWithTasks]);
8994

9095
// Keep width reactive without putting window.innerWidth in deps (which never triggers)
9196
useEffect(() => {
@@ -718,7 +723,7 @@ const TeamMemberTasks = React.memo(props => {
718723
</thead>
719724

720725
<tbody className={darkMode ? styles.darkTbody : ''}>
721-
{teamList.length === 0 ? (
726+
{teamList.length === 0 || !selectionsLoaded ? (
722727
<SkeletonLoading
723728
template="TeamMemberTasks"
724729
data-testid="skeleton-loading-team-member-tasks-row"

src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen, fireEvent } from '@testing-library/react';
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
22
import '@testing-library/jest-dom/extend-expect';
33
import thunk from 'redux-thunk';
44
import { configureStore } from 'redux-mock-store';
@@ -88,12 +88,24 @@ const store = mockStore({
8888

8989
vi.mock('axios');
9090

91+
beforeEach(() => {
92+
axios.get.mockResolvedValue({
93+
status: 200,
94+
data: { items: [] },
95+
});
96+
axios.post.mockResolvedValue({
97+
status: 200,
98+
data: { selections: {} },
99+
});
100+
});
101+
91102
describe('TeamMemberTasks component', () => {
92103
it('renders without crashing', () => {
93104
axios.get.mockResolvedValue({
94105
status: 200,
95106
data: '',
96107
});
108+
axios.post.mockResolvedValue({ status: 200, data: { selections: {} } });
97109
render(
98110
<Provider store={store}>
99111
<MemoryRouter>
@@ -225,7 +237,7 @@ describe('TeamMemberTasks component', () => {
225237

226238
expect(screen.getAllByTestId('team-member-tasks-row')).not.toHaveLength(0);
227239
});
228-
it('check if the skeleton loading html elements are not shown when isLoading is false', () => {
240+
it('check if the skeleton loading html elements are not shown when isLoading is false', async () => {
229241
axios.get.mockResolvedValue({
230242
status: 200,
231243
data: '',
@@ -238,7 +250,9 @@ describe('TeamMemberTasks component', () => {
238250
</MemoryRouter>
239251
</Provider>,
240252
);
241-
expect(screen.queryByTestId('team-member-tasks-row')).not.toBeInTheDocument();
253+
await waitFor(() => {
254+
expect(screen.queryAllByTestId('team-member-tasks-row')).toHaveLength(0);
255+
});
242256
});
243257
it('check if class names does not include color when dark mode is false', () => {
244258
axios.get.mockResolvedValue({

src/components/UserProfile/UserProfileEdit/__tests__/UserProfileEdit.test.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@ describe("<UserProfileEdit />", () => {
4747
allProjects: mockProps.allProjects,
4848
theme: mockProps.theme,
4949
});
50+
const axios = require('axios');
51+
axios.get = vi.fn().mockResolvedValue({ status: 200, data: '' });
52+
axios.post = vi.fn().mockResolvedValue({ status: 200, data: {} });
5053
});
5154

55+
56+
5257
it("renders the UserProfileEdit component", async () => {
5358
render(
5459
<Provider store={store}>
@@ -62,7 +67,7 @@ describe("<UserProfileEdit />", () => {
6267
expect(mockProps.getUserProfile).toHaveBeenCalledWith("12345")
6368
);
6469
expect(screen.getByText(/Save Changes/i)).toBeInTheDocument();
65-
});
70+
}, 10000);
6671

6772
it("displays loading spinner initially", () => {
6873
render(
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import PropTypes from 'prop-types';
2+
import { COLOR_SWATCHES } from './constants';
3+
import styles from './UserState.module.css';
4+
5+
function ColorPicker({ selectedColor, onSelect }) {
6+
return (
7+
<div className={styles.colorPickerWrapper}>
8+
<span className={styles.colorPickerLabel}>Color:</span>
9+
{COLOR_SWATCHES.map(swatch => (
10+
<button
11+
key={swatch.hex}
12+
type="button"
13+
title={swatch.label}
14+
onClick={() => onSelect(swatch.hex)}
15+
className={styles.colorSwatch}
16+
style={{
17+
background: swatch.hex,
18+
border: selectedColor === swatch.hex ? '3px solid #000' : '2px solid transparent',
19+
outline: selectedColor === swatch.hex ? '2px solid #fff' : 'none',
20+
outlineOffset: '-4px',
21+
}}
22+
/>
23+
))}
24+
</div>
25+
);
26+
}
27+
28+
ColorPicker.propTypes = {
29+
selectedColor: PropTypes.string.isRequired,
30+
onSelect: PropTypes.func.isRequired,
31+
};
32+
33+
export default ColorPicker;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import PropTypes from 'prop-types';
2+
import { EMOJI_OPTIONS } from './constants';
3+
import styles from './UserState.module.css';
4+
5+
function EmojiPicker({ darkMode, onSelect }) {
6+
return (
7+
<div className={`${styles.emojiPickerGrid} ${darkMode ? styles.dark : styles.light}`}>
8+
{EMOJI_OPTIONS.map(emoji => (
9+
<button
10+
key={emoji}
11+
type="button"
12+
onClick={() => onSelect(emoji)}
13+
className={styles.emojiBtn}
14+
>
15+
{emoji}
16+
</button>
17+
))}
18+
</div>
19+
);
20+
}
21+
22+
EmojiPicker.propTypes = {
23+
darkMode: PropTypes.bool.isRequired,
24+
onSelect: PropTypes.func.isRequired,
25+
};
26+
27+
export default EmojiPicker;

0 commit comments

Comments
 (0)