Skip to content

Commit

Permalink
merge master branch
Browse files Browse the repository at this point in the history
  • Loading branch information
Terala-Priyanka committed Jan 25, 2024
2 parents 447c2fa + 9bf8232 commit 45f4e59
Show file tree
Hide file tree
Showing 17 changed files with 479 additions and 27 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* Create new permission 'Users: Can view profile pictures'. Refs UIU-3018.
* Format currency values as currencies, not numbers. Refs UIU-2026.
* Show country name in user address instead of country id. Refs UIU-2976.
* Add patron notice print jobs to action menu. Refs UIU-3029.
* Update sub permissions of permission 'Users: Can view user profiles'. Refs UIU-3038.
* UserInformation in UserDetails to display profile picture. Refs UIU-3011.

## [10.0.4](https://github.com/folio-org/ui-users/tree/v10.0.4) (2023-11-10)
Expand Down
27 changes: 25 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@
"loan-policy-storage": "1.0 2.0",
"loan-storage": "4.0 5.0 6.0 7.0",
"notes": "2.0 3.0",
"request-storage": "2.5 3.0 4.0 5.0 6.0"
"request-storage": "2.5 3.0 4.0 5.0 6.0",
"batch-print": "1.0"

},
"permissionSets": [
{
Expand Down Expand Up @@ -985,7 +987,8 @@
"description": "Also includes basic permissions to view users",
"subPermissions": [
"ui-users.view",
"users.profile-picture.item.get"
"users.profile-picture.item.get",
"users.configurations.item.get"
],
"visible": true
},
Expand Down Expand Up @@ -1022,6 +1025,26 @@
"ui-users.settings.patron-blocks.view"
],
"visible": true
},
{
"permissionName": "ui-users.view-patron-notice-print-jobs",
"displayName": "Users: View patron notice print jobs",
"subPermissions": [
"ui-users.view",
"mod-batch-print.entries.collection.get",
"mod-batch-print.entries.item.get",
"mod-batch-print.print.read"
],
"visible": true
},
{
"permissionName": "ui-users.remove-patron-notice-print-jobs",
"displayName": "Users: View and remove patron notice print jobs",
"subPermissions": [
"ui-circulation.settings.view-patron-notice-print-jobs",
"mod-batch-print.entries.item.delete"
],
"visible": true
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';

import { IfPermission } from '@folio/stripes/core';
import {
Button,
Icon,
} from '@folio/stripes/components';

const PatronNoticePrintJobsLink = () => {
const history = useHistory();

return (
<IfPermission perm="ui-users.view-patron-notice-print-jobs">
<Button
data-testid="patronNoticePrintJobsLink"
to={{
pathname: '/users/patron-notice-print-jobs',
state: {
pathname: history?.location?.pathname,
search: history?.location?.search,
},
}}
buttonStyle="dropdownItem"
>
<Icon icon="download">
<FormattedMessage id="ui-users.actionMenu.patronNoticePrintJobs" />
</Icon>
</Button>
</IfPermission>
);
};


export default PatronNoticePrintJobsLink;
1 change: 1 addition & 0 deletions src/components/PatronNoticePrintJobsLink/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './PatronNoticePrintJobsLink';
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ class UsersRouting extends React.Component {
<Route path={`${base}/:id/patronblocks/create`} component={Routes.PatronBlockContainer} />
<Route path={`${base}/create`} component={Routes.UserEditContainer} />
<Route path={`${base}/lost-items`} component={Routes.LostItemsContainer} />
<Route path={`${base}/patron-notice-print-jobs`} component={Routes.PatronNoticePrintJobsContainer} />
<Route path={`${base}/:id/edit`} component={Routes.UserEditContainer} />
<Route path={`${base}/view/:id`} component={Routes.UserDetailFullscreenContainer} />
<Route path={`${base}/notes/new`} exact component={NoteCreatePage} />
Expand Down
68 changes: 68 additions & 0 deletions src/routes/PatronNoticePrintJobsContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@

import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';

import { stripesConnect } from '@folio/stripes/core';

import PatronNoticePrintJobs from '../views/PatronNoticePrintJobs';

const PatronNoticePrintJobsContainer = (props) => {
const { mutator, resources } = props;
const records = resources?.entries?.records;
const history = useHistory();

const onClose = () => {
const { location } = props;

if (location.state) {
history.goBack();
} else {
history.push('/users?sort=name');
}
};

return (
<PatronNoticePrintJobs
records={records}
mutator={mutator}
onClose={onClose}
/>
);
};

PatronNoticePrintJobsContainer.manifest = {
entries: {
type: 'okapi',
path: 'print/entries',
params: {
query: 'type="BATCH"',
sortby: 'created/sort.descending'
},
records: 'items',
throwErrors: false,
},
printingJob: {
type: 'okapi',
path: 'print/entries',
accumulate: 'true',
fetch: false,
throwErrors: false,
},
};

PatronNoticePrintJobsContainer.propTypes = {
resources: PropTypes.shape({
entries: PropTypes.shape({
records: PropTypes.arrayOf(PropTypes.object),
}),
}).isRequired,
mutator: PropTypes.shape({
printingJob: PropTypes.shape({
GET: PropTypes.func,
reset: PropTypes.func,
}),
}).isRequired,
location: PropTypes.object,
};

export default stripesConnect(PatronNoticePrintJobsContainer);
69 changes: 69 additions & 0 deletions src/routes/PatronNoticePrintJobsContainer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { waitFor, fireEvent } from '@folio/jest-config-stripes/testing-library/react';

import renderWithRouter from 'helpers/renderWithRouter';
import PatronNoticePrintJobsContainer from './PatronNoticePrintJobsContainer';


jest.mock('../views/PatronNoticePrintJobs', () => {
return jest.fn(({ onClose }) => (<button type="button" data-testid="close-button" onClick={onClose}>Close</button>));
});

jest.mock('history', () => {
return {
createMemoryHistory: jest.fn(() => ({
push: jest.fn(),
location: {},
listen: jest.fn(),
goBack: jest.fn(),
})),
};
});

const mockMutator = {
printingJob: {
GET: jest.fn(),
reset: jest.fn(),
},
};

const mockResources = {
entries: {
records: [],
},
};

const renderPatronNoticePrintJobsContainer = (extraProps) => renderWithRouter(
<PatronNoticePrintJobsContainer mutator={mockMutator} resources={mockResources} {...extraProps} />
);

describe('PatronNoticePrintJobsContainer', () => {
it('should render PatronNoticePrintJobs', () => {
const { container } = renderPatronNoticePrintJobsContainer();
expect(container).toBeInTheDocument();
});

it('should go back if location state exists', async () => {
const { getByTestId, history } = renderPatronNoticePrintJobsContainer({
location: { state: { from: '/previous-page' } }
});

fireEvent.click(getByTestId('close-button'));

await waitFor(() => {
expect(history.goBack).toHaveBeenCalled();
});
});


it('should redirect to /users?sort=name if location state does not exist', async () => {
const { getByTestId, history } = renderPatronNoticePrintJobsContainer({
location: {}
});

fireEvent.click(getByTestId('close-button'));

await waitFor(() => {
expect(history.push).toHaveBeenCalled();
});
});
});
1 change: 1 addition & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { default as LoansListingContainer } from './LoansListingContainer';
export { default as LoanDetailContainer } from './LoanDetailContainer';
export { default as AccountDetailsContainer } from './AccountDetailsContainer';
export { default as LostItemsContainer } from './LostItemsContainer';
export { default as PatronNoticePrintJobsContainer } from './PatronNoticePrintJobsContainer';
Empty file.
150 changes: 150 additions & 0 deletions src/views/PatronNoticePrintJobs/PatronNoticePrintJobs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@

import { orderBy } from 'lodash';
import { Button, Pane, MenuSection, MultiColumnList, Checkbox, FormattedDate, FormattedTime, TextLink } from '@folio/stripes/components';
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { useOkapiKy, useCallout, useStripes } from '@folio/stripes/core';

import css from './PatronNoticePrintJobs.css';

const ASC = 'ascending';
const DESC = 'descending';
const visibleColumns = ['id', 'created'];

export const generateFormatter = (markPrintJobForDeletion, openPDF) => {
return {
id: (item) => (
<Checkbox
type="checkbox"
checked={item.selected}
onChange={() => markPrintJobForDeletion(item)}
/>
),
created: (item) => (
<TextLink className={css.printJobLink} onClick={() => openPDF(item)}>
<FormattedDate value={item.created} /> <FormattedTime value={item.created} />
</TextLink>
)
};
};

const PatronNoticePrintJobs = (props) => {
const { records, mutator, onClose } = props;
const [contentData, setContentData] = useState([]);
const [sortOrder, setSortOrder] = useState(DESC);
const [allSelected, toggleSelectAll] = useState(false);
const sort = () => setSortOrder(sortOrder === DESC ? ASC : DESC);
const ky = useOkapiKy();
const callout = useCallout();
const stripes = useStripes();

const markPrintJobForDeletion = (item) => {
const clonedData = [...contentData];
const index = clonedData.findIndex(el => el.id === item.id);

clonedData[index] = { ...item, selected: !item.selected };
setContentData(clonedData);
};

const markAllPrintJobForDeletions = () => {
toggleSelectAll(!allSelected);
const clonedData = contentData.map(el => ({ ...el, selected: !allSelected }));
setContentData(clonedData);
};

const openPDF = async (item) => {
try {
mutator.printingJob.reset();
const { content } = await mutator.printingJob.GET({ path: `print/entries/${item.id}` });
const bytes = new Uint8Array(content.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
const blob = new Blob([bytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);

window.open(url, '_blank');
} catch (error) {
callout.sendCallout({
message: <FormattedMessage id="ui-users.patronNoticePrintJobs.errors.pdf" />,
type: 'error',
});
}
};

useEffect(() => {
const updatedRecords =
orderBy(records, (item) => item.created, sortOrder === DESC ? 'desc' : 'asc')
.map(record => ({
...record,
selected: !!record.selected,
}));

setContentData(updatedRecords);
}, [records, sortOrder]);

const formatter = generateFormatter(markPrintJobForDeletion, openPDF);
const columnMapping = {
id: <Checkbox type="checkbox" onChange={() => markAllPrintJobForDeletions()} />,
created: <FormattedMessage id="ui-users.patronNoticePrintJobs.created" />,
};

const renderActionMenu = ({ onToggle }) => {
const removeSelectedPrintJobs = async () => {
const selectedJobs = contentData.filter(item => item.selected);
const ids = selectedJobs.map(job => job.id).join(',');
const filtered = contentData.filter(item => !item.selected);

await ky.delete(`print/entries?ids=${ids}`);

setContentData(filtered);
onToggle();
toggleSelectAll(false);
};

return (
<MenuSection label={<FormattedMessage id="ui-users.patronNoticePrintJobs.actions" />}>
<Button buttonStyle="dropdownItem" onClick={removeSelectedPrintJobs}>
<FormattedMessage id="ui-users.patronNoticePrintJobs.actions.delete" />
</Button>
</MenuSection>
);
};

const actionMenu = stripes?.hasPerm('ui-users.remove-patron-notice-print-jobs') ? renderActionMenu : null;

return (
<Pane
paneTitle={
<FormattedMessage id="ui-users.patronNoticePrintJobs.label" />
}
defaultWidth="fill"
dismissible
actionMenu={actionMenu}
onClose={onClose}
>
<MultiColumnList
contentData={contentData}
formatter={formatter}
visibleColumns={visibleColumns}
columnMapping={columnMapping}
onHeaderClick={sort}
sortDirection={sortOrder}
sortedColumn="created"
nonInteractiveHeaders={['id']}
interactive={false}
/>
</Pane>
);
};

PatronNoticePrintJobs.propTypes = {
records: PropTypes.arrayOf(PropTypes.object),
onClose: PropTypes.func,
mutator: PropTypes.shape({
printingJob: PropTypes.shape({
GET: PropTypes.func,
reset: PropTypes.func,
}),
}).isRequired,
};

export default PatronNoticePrintJobs;
Loading

0 comments on commit 45f4e59

Please sign in to comment.