Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIU-3029: Add patron notice print jobs to action menu #2614

Merged
merged 7 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* 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.

## [10.0.4](https://github.com/folio-org/ui-users/tree/v10.0.4) (2023-11-10)
Expand Down
24 changes: 23 additions & 1 deletion 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 @@ -1023,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
Loading