My apologies for the "ugly" version.
After reacquainting myself with redux and how it is used with Volto, I implemented the "correct" version.
Steps involved:
- Register the new reducer, action and type
- Update my volto
config.js
to add the userfullname
reducer.
- Use the new
getUserName
action in my code
Step 1 - Register reducer, action and action type
Here is my userfullname.js
reducer file:
Located at:
reducers
├── index.js
└── userfullname
└── userfullname.js <-------------------
import { GET_USER_NAME } from '../../constants/ActionTypes';
const initialState = {
userNames: {}, // <---- the userNames state is going to be important moving forward
};
export default function users(state = initialState, action = {}) {
switch (action.type) {
case `${GET_USER_NAME}_SUCCESS`:
return {
...state,
userNames: {
...state.userNames,
[action.request.path.split('/').pop()]: action.result.fullname,
},
};
default:
return state;
}
}
The ActionTypes.js
is located under the constants
folder
constants
└── ActionTypes.js
and look like this:
// User actions
export const GET_USER_NAME = 'GET_USER_NAME';
export const GET_USER_NAME_SUCCESS = 'GET_USER_NAME_SUCCESS';
export const GET_USER_NAME_FAILURE = 'GET_USER_NAME_FAILURE';
Here is the userfullname.js
file with the action, this maps the location of the actual endpoint:
actions
├── index.js
└── userfullname.js <-------
import { GET_USER_NAME } from '../constants/ActionTypes';
export function getUserName(userId) {
return {
type: GET_USER_NAME,
request: {
op: 'get',
path: `/@user-name/${userId}`,
},
};
}
Step 2 - Update the Volto config.js
Here are the changes to the config.js
// the key changes...
import userfullname from './reducers/users/userfullname';
export default function applyConfig(config) {
// Add here your project's configuration here by modifying `config` accordingly
/*
*/
config.addonReducers.userfullname = userfullname; // <---- implied here is the userNames state
//...
return config;
}
Step 3 - Use the new action and reducer with userNames state in my code
Note that we need both the action and the reducer (in the form of the userNames state, retrieved using useSelector)
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { Card, Grid, List, Button } from 'semantic-ui-react';
import { FaFilePdf } from 'react-icons/fa';
import moment from 'moment';
import { getUserName } from '../../../../actions/userfullname';
import './gridlist.css';
const GridListTemplate = ({ items }) => {
const dispatch = useDispatch();
const userNames = useSelector((state) => state.userfullname.userNames); <--- and this is where we make use of the userNames state
useEffect(() => {
const allCreators = [
...new Set(items.flatMap((item) => item.listCreators || [])),
];
allCreators.forEach((userId) => {
if (!userNames[userId]) {
dispatch(getUserName(userId));
}
});
}, [items, userNames, dispatch]);
return (
<Grid columns={2}>
{items.map((item, index) => {
const publicationDate = item.Date
? moment(item.Date).format('MMMM D, YYYY')
: 'No publication date';
const documentType =
item.document_type || item['Type'] || item['@type'];
const folderName =
item.parentTitle ||
(item['@id']
? item['@id'].split('/').slice(-2, -1)[0]
: 'Unknown folder');
const contributors = item.listCreators || [];
return (
<Grid.Column key={`item-index-${index}`}>
<Card fluid>
<Card.Content className="grid-list-content">
<Card.Header>
{item.review_state === 'internally_published' && (
<div className="grid-list-sash">Members Only</div>
)}
<h4 className="grid-label">{documentType}</h4>
<h3 className="grid-list-location">{folderName}</h3>
<a
className="grid-list-heading"
title={item.title}
href={item.getURL}
>
{item.title}
</a>
</Card.Header>
<Card.Meta>
by
<span className="list-creators">
{' '}
{contributors
.map((userId) => userNames[userId] || userId)
.join(', ')}
</span>
{publicationDate}
</Card.Meta>
<Card.Description className="grid-list-description">
{item.description}
</Card.Description>
{item.hasFile && (
<Card.Content extra className="grid-list-footer">
<a href={`${item.getURL}/@@display-file/file`}>
<FaFilePdf
className="grid-list-icon"
style={{ marginRight: '5px' }}
/>
</a>
{item.countChildren > 0 && (
<>
<span> &</span>
<Button
as="a"
href={item.getURL}
size="small"
className="grid-list-button"
style={{ marginLeft: '10px' }}
>
Supporting Materials ({item.countChildren})
</Button>
</>
)}
</Card.Content>
)}
</Card.Content>
</Card>
</Grid.Column>
);
})}
</Grid>
);
};
GridListTemplate.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
Date: PropTypes.string,
document_type: PropTypes.string,
Type: PropTypes.string,
'@type': PropTypes.string,
parentTitle: PropTypes.string,
'@id': PropTypes.string,
title: PropTypes.string,
getURL: PropTypes.string,
listCreators: PropTypes.arrayOf(PropTypes.string),
review_state: PropTypes.string,
description: PropTypes.string,
hasFile: PropTypes.bool,
countChildren: PropTypes.number,
}),
).isRequired,
linkMore: PropTypes.any,
isEditMode: PropTypes.bool,
};
export default GridListTemplate;
For context this is how my Volto project is structured:
src
├── actions <--------- the actions go here
├── addons
├── components
│ ├── Blocks
│ ├── ListingVarations
│ │ ├── CommitteeListTemplate
│ │ ├── GatedContentListTemplate
│ │ ├── GridListTemplate <------- the `GridListTemplate.jsx` file where all the action happens is in this folder
│ └── ResourceGridTemplate
├── config.js <--------- where I register the reducer
└── reducers
└── userfullname <---------- added the `userfullname.js` file here
update: I got into some namespace trouble and had to change the namespace to be userfullname
(I was previously using users
and this was clashing with something else in the redux app store and clashing with the PersonalTools menu, at the bottom left of the interface, which already relies on the users
namespace.