Diff
checker
Text
Text
Images
Documents
Excel
Folders
Legal
Enterprise
Desktop
Pricing
Sign in
Download Diffchecker Desktop
Compare text
Find the difference between two text files
Tools
History
Real-time editor
Hide unchanged lines
Disable line wrap
Layout
Split
Unified
Diff precision
Smart
Word
Char
Syntax highlighting
Choose syntax
Ignore
Transform text
Go to first change
Edit input
Diffchecker Desktop
The most secure way to run Diffchecker. Get the Diffchecker Desktop app: your diffs never leave your computer!
Get Desktop
Search/Browse
Created
4 years ago
Diff never expires
Clear
Export
Share
Explain
345 removals
Lines
Total
Removed
Characters
Total
Removed
To continue using this feature, upgrade to
Diff
checker
Pro
View Pricing
700 lines
Copy
447 additions
Lines
Total
Added
Characters
Total
Added
To continue using this feature, upgrade to
Diff
checker
Pro
View Pricing
807 lines
Copy
import React from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import PropTypes from 'prop-types';
import {
import {
omit,
omit,
get,
get,
set,
set,
flowRight,
flowRight,
size,
size,
Copy
Copied
Copy
Copied
isEmpty,
noop,
} from 'lodash';
} from 'lodash';
import {
import {
injectIntl,
injectIntl,
FormattedMessage,
FormattedMessage,
} from 'react-intl';
} from 'react-intl';
Copy
Copied
Copy
Copied
import saveAs from 'file-saver';
import moment from 'moment';
import {
import {
Copy
Copied
Copy
Copied
Pluggable,
AppIcon,
AppIcon,
Copy
Copied
Copy
Copied
IfPermission,
IfInterface,
CalloutContext,
CalloutContext,
stripesConnect,
stripesConnect,
withNamespace,
withNamespace,
} from '@folio/stripes/core';
} from '@folio/stripes/core';
import { SearchAndSort } from '@folio/stripes/smart-components';
import { SearchAndSort } from '@folio/stripes/smart-components';
import {
import {
Copy
Copied
Copy
Copied
Button,
Icon,
Icon,
Checkbox,
Checkbox,
Copy
Copied
Copy
Copied
MenuSection,
checkScope,
checkScope,
HasCommand,
HasCommand,
} from '@folio/stripes/components';
} from '@folio/stripes/components';
import { pagingTypes } from '@folio/stripes-components/lib/MultiColumnList';
import { pagingTypes } from '@folio/stripes-components/lib/MultiColumnList';
Copy
Copied
Copy
Copied
import FilterNavigation from '../../components/FilterNavigation';
import packageInfo from '../../../package';
import packageInfo from '../../../package';
import InstanceForm from '../../edit/InstanceForm';
import InstanceForm from '../../edit/InstanceForm';
import ViewInstanceWrapper from '../../ViewInstanceWrapper';
import ViewInstanceWrapper from '../../ViewInstanceWrapper';
import formatters from '../../referenceFormatters';
import formatters from '../../referenceFormatters';
import withLocation from '../../withLocation';
import withLocation from '../../withLocation';
Copy
Copied
Copy
Copied
import CheckboxColumn from '../../components/InstancesList/CheckboxColumn';
import {
import {
getCurrentFilters,
getCurrentFilters,
getNextSelectedRowsState,
getNextSelectedRowsState,
parseFiltersToStr,
parseFiltersToStr,
marshalInstance,
marshalInstance,
omitFromArray,
omitFromArray,
Copy
Copied
Copy
Copied
isTestEnv,
handleKeyCommand,
handleKeyCommand,
} from '../../utils';
} from '../../utils';
import {
import {
Copy
Copied
Copy
Copied
INSTANCES_ID_REPORT_TIMEOUT,
QUICK_EXPORT_LIMIT,
segments,
segments,
Copy
Copied
Copy
Copied
browseModeOptions,
FACETS,
} from '../../constants';
} from '../../constants';
import {
import {
Copy
Copied
Copy
Copied
IdReportGenerator,
InTransitItemsReport,
} from '../../reports';
import ErrorModal from '../../components/ErrorModal';
import CheckboxColumn from '../../components/InstancesList/CheckboxColumn';
import SelectedRecordsModal from '../../components/SelectedRecordsModal';
import ImportRecordModal from '../../components/ImportRecordModal';
import { buildQuery } from '../../routes/buildManifestObject';
import {
getItem,
getItem,
setItem,
setItem,
} from '../../storage';
} from '../../storage';
import facetsStore from '../../stores/facetsStore';
import facetsStore from '../../stores/facetsStore';
import css from '../../components/InstancesList/instances.css';
import css from '../../components/InstancesList/instances.css';
Copy
Copied
Copy
Copied
import { getFilterConfig } from '../../filterConfig';
import { getFilterConfig } from '../../filterConfig';
import SearchBrowseNavigation from '../../components/SearchBrowseNavigation/SearchBrowseNavigation';
import SearchBrowseNavigation from '../../components/SearchBrowseNavigation/SearchBrowseNavigation';
const INITIAL_RESULT_COUNT = 30;
const INITIAL_RESULT_COUNT = 30;
const RESULT_COUNT_INCREMENT = 30;
const RESULT_COUNT_INCREMENT = 30;
const columnSets = {
const columnSets = {
SUBJECTS: ['subject', 'numberOfTitles'],
SUBJECTS: ['subject', 'numberOfTitles'],
CALL_NUMBERS: ['callNumber', 'title', 'numberOfTitles'],
CALL_NUMBERS: ['callNumber', 'title', 'numberOfTitles'],
CONTRIBUTORS: ['contributor', 'contributorType', 'relatorTerm', 'numberOfTitles'],
CONTRIBUTORS: ['contributor', 'contributorType', 'relatorTerm', 'numberOfTitles'],
};
};
const TOGGLEABLE_COLUMNS = ['contributors', 'publishers', 'relation'];
const TOGGLEABLE_COLUMNS = ['contributors', 'publishers', 'relation'];
const NON_TOGGLEABLE_COLUMNS = ['select', 'title'];
const NON_TOGGLEABLE_COLUMNS = ['select', 'title'];
const ALL_COLUMNS = Array.from(new Set([
const ALL_COLUMNS = Array.from(new Set([
...NON_TOGGLEABLE_COLUMNS,
...NON_TOGGLEABLE_COLUMNS,
...TOGGLEABLE_COLUMNS,
...TOGGLEABLE_COLUMNS,
...columnSets.CALL_NUMBERS,
...columnSets.CALL_NUMBERS,
...columnSets.SUBJECTS,
...columnSets.SUBJECTS,
...columnSets.CONTRIBUTORS,
...columnSets.CONTRIBUTORS,
]));
]));
const VISIBLE_COLUMNS_STORAGE_KEY = 'inventory-visible-columns';
const VISIBLE_COLUMNS_STORAGE_KEY = 'inventory-visible-columns';
Copy
Copied
Copy
Copied
class
InstancesList
extends React.Component {
class
SearchView
extends React.Component {
static defaultProps = {
static defaultProps = {
Copy
Copied
Copy
Copied
browseOnly: false,
showSingleResult: true,
showSingleResult: true,
};
};
static propTypes = {
static propTypes = {
data: PropTypes.object,
data: PropTypes.object,
parentResources: PropTypes.object,
parentResources: PropTypes.object,
parentMutator: PropTypes.object,
parentMutator: PropTypes.object,
showSingleResult: PropTypes.bool,
showSingleResult: PropTypes.bool,
Copy
Copied
Copy
Copied
browseOnly: PropTypes.bool,
disableRecordCreation: PropTypes.bool,
disableRecordCreation: PropTypes.bool,
updateLocation: PropTypes.func.isRequired,
updateLocation: PropTypes.func.isRequired,
goTo: PropTypes.func.isRequired,
goTo: PropTypes.func.isRequired,
getParams: PropTypes.func.isRequired,
getParams: PropTypes.func.isRequired,
segment: PropTypes.string,
segment: PropTypes.string,
intl: PropTypes.object,
intl: PropTypes.object,
match: PropTypes.shape({
match: PropTypes.shape({
path: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
params: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
namespace: PropTypes.string,
namespace: PropTypes.string,
renderFilters: PropTypes.func.isRequired,
renderFilters: PropTypes.func.isRequired,
searchableIndexes: PropTypes.arrayOf(PropTypes.object).isRequired,
searchableIndexes: PropTypes.arrayOf(PropTypes.object).isRequired,
mutator: PropTypes.shape({
mutator: PropTypes.shape({
query: PropTypes.shape({
query: PropTypes.shape({
update: PropTypes.func.isRequired,
update: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired,
}).isRequired,
}).isRequired,
}),
}),
location: PropTypes.shape({
location: PropTypes.shape({
search: PropTypes.string
search: PropTypes.string
}),
}),
stripes: PropTypes.object.isRequired,
stripes: PropTypes.object.isRequired,
fetchFacets: PropTypes.func,
fetchFacets: PropTypes.func,
};
};
static contextType = CalloutContext;
static contextType = CalloutContext;
static manifest = Object.freeze({
static manifest = Object.freeze({
query: {},
query: {},
});
});
constructor(props) {
constructor(props) {
super(props);
super(props);
this.state = {
this.state = {
Copy
Copied
Copy
Copied
showNewFastAddModal: false,
inTransitItemsExportInProgress: false,
instancesIdExportInProgress: false,
holdingsIdExportInProgress: false,
instancesQuickExportInProgress: false,
showErrorModal: false,
selectedRows: {},
selectedRows: {},
Copy
Copied
Copy
Copied
optionSelected: '',
isSelectedRecordsModalOpened: false,
visibleColumns: this.getInitialToggableColumns(),
isImportRecordModalOpened: false,
searchAndSortKey: 0,
searchAndSortKey: 0,
isSingleResult: this.props.showSingleResult,
isSingleResult: this.props.showSingleResult,
};
};
}
}
Copy
Copied
Copy
Copied
componentDidMount() {
if (this.getSelectedBrowseOption() || this.getExecutedBrowseQuery()) {
this.setState({
optionSelected: this.getQIndexFromParams(),
isSingleResult: false,
});
} else {
this.setState({
optionSelected: '',
});
Text moved to lines 379-381
}
}
getQIndexFromParams = () => {
getQIndexFromParams = () => {
const params = new URLSearchParams(this.props.location.search);
const params = new URLSearchParams(this.props.location.search);
return params.get('qindex');
return params.get('qindex');
}
}
Copy
Copied
Copy
Copied
getSelectedBrowseOption = () => {
const isBrowseSelectedBasedOnState = Object.keys(browseModeOptions).filter(k => browseModeOptions[k] === this.state.optionSelected)[0];
return isBrowseSelectedBasedOnState;
}
getExecutedBrowseQuery = () => {
const isBrowseSelectedBasedOnUrl = Object.keys(browseModeOptions).filter(k => browseModeOptions[k] === this.getQIndexFromParams())[0];
return isBrowseSelectedBasedOnUrl;
}
getInitialToggableColumns = () => {
getInitialToggableColumns = () => {
return getItem(VISIBLE_COLUMNS_STORAGE_KEY) || TOGGLEABLE_COLUMNS;
return getItem(VISIBLE_COLUMNS_STORAGE_KEY) || TOGGLEABLE_COLUMNS;
}
}
getVisibleColumns = () => {
getVisibleColumns = () => {
// display particular columnset based on the quindex value in the URL, since the search has been
// display particular columnset based on the quindex value in the URL, since the search has been
// performed.
// performed.
Copy
Copied
Copy
Copied
const executedBrowseQuery = Object.keys(browseModeOptions).filter(k => browseModeOptions[k] === this.getQIndexFromParams())[0];
const columns = this.state.visibleColumns;
const columns = columnSets[executedBrowseQuery];
const visibleColumns = new Set([...
columns, ...
NON_TOGGLEABLE_COLUMNS]);
if (!executedBrowseQuery) {
return ALL_COLUMNS.filter(key => visibleColumns.has(key));
const visibleColumns = new Set([...
NON_TOGGLEABLE_COLUMNS]);
return ALL_COLUMNS.filter(key => visibleColumns.has(key));
} else {
const visibleColumns = new Set([...columns]);
return Array.from(visibleColumns);
}
}
}
onFilterChangeHandler = ({ name, values }) => {
onFilterChangeHandler = ({ name, values }) => {
const {
const {
data: { query },
data: { query },
match: { path },
match: { path },
goTo,
goTo,
getParams,
getParams,
} = this.props;
} = this.props;
const curFilters = getCurrentFilters(get(query, 'filters', ''));
const curFilters = getCurrentFilters(get(query, 'filters', ''));
const mergedFilters = values.length
const mergedFilters = values.length
? { ...curFilters, [name]: values }
? { ...curFilters, [name]: values }
: omit(curFilters, name);
: omit(curFilters, name);
const filtersStr = parseFiltersToStr(mergedFilters);
const filtersStr = parseFiltersToStr(mergedFilters);
const params = getParams();
const params = getParams();
goTo(path, { ...params, filters: filtersStr });
goTo(path, { ...params, filters: filtersStr });
};
};
closeNewInstance = (e) => {
closeNewInstance = (e) => {
if (e) e.preventDefault();
if (e) e.preventDefault();
this.setState({ copiedInstance: null });
this.setState({ copiedInstance: null });
this.props.updateLocation({ layer: null });
this.props.updateLocation({ layer: null });
};
};
createInstance = (instanceData) => {
createInstance = (instanceData) => {
const { data: { identifierTypesByName } } = this.props;
const { data: { identifierTypesByName } } = this.props;
// Massage record to add preceeding and succeeding title fields
// Massage record to add preceeding and succeeding title fields
const instance = marshalInstance(instanceData, identifierTypesByName);
const instance = marshalInstance(instanceData, identifierTypesByName);
// POST item record
// POST item record
return this.props.parentMutator.records.POST(instance);
return this.props.parentMutator.records.POST(instance);
};
};
onCreate = (instance) => {
onCreate = (instance) => {
return this.createInstance(instance).then(() => this.closeNewInstance());
return this.createInstance(instance).then(() => this.closeNewInstance());
}
}
openCreateInstance = () => {
openCreateInstance = () => {
this.props.updateLocation({ layer: 'create' });
this.props.updateLocation({ layer: 'create' });
}
}
copyInstance = (instance) => {
copyInstance = (instance) => {
const {
const {
precedingTitles,
precedingTitles,
succeedingTitles,
succeedingTitles,
childInstances,
childInstances,
parentInstances,
parentInstances,
} = instance;
} = instance;
let copiedInstance = omit(instance, ['id', 'hrid']);
let copiedInstance = omit(instance, ['id', 'hrid']);
if (precedingTitles?.length) {
if (precedingTitles?.length) {
copiedInstance.precedingTitles = omitFromArray(precedingTitles, 'id');
copiedInstance.precedingTitles = omitFromArray(precedingTitles, 'id');
}
}
if (succeedingTitles?.length) {
if (succeedingTitles?.length) {
copiedInstance.succeedingTitles = omitFromArray(succeedingTitles, 'id');
copiedInstance.succeedingTitles = omitFromArray(succeedingTitles, 'id');
}
}
if (childInstances?.length) {
if (childInstances?.length) {
copiedInstance.childInstances = omitFromArray(childInstances, 'id');
copiedInstance.childInstances = omitFromArray(childInstances, 'id');
}
}
if (parentInstances?.length) {
if (parentInstances?.length) {
copiedInstance.parentInstances = omitFromArray(parentInstances, 'id');
copiedInstance.parentInstances = omitFromArray(parentInstances, 'id');
}
}
copiedInstance = set(copiedInstance, 'source', 'FOLIO');
copiedInstance = set(copiedInstance, 'source', 'FOLIO');
this.setState({ copiedInstance });
this.setState({ copiedInstance });
this.openCreateInstance();
this.openCreateInstance();
}
}
Copy
Copied
Copy
Copied
refocusOnInputSearch = (
segment
) => {
refocusOnInputSearch = (
) => {
// when navigation button is clicked to change the search segment
// when navigation button is clicked to change the search segment
// the focus stays on the button so refocus back on the input search.
// the focus stays on the button so refocus back on the input search.
// https://issues.folio.org/browse/UIIN-1358
// https://issues.folio.org/browse/UIIN-1358
Copy
Copied
Copy
Copied
if (segment !== segments.instances) {
this.setState({
optionSelected: ''
});
}
facetsStore.getState().resetFacetSettings();
facetsStore.getState().resetFacetSettings();
document.getElementById('input-inventory-search').focus();
document.getElementById('input-inventory-search').focus();
}
}
renderNavigation = () => (
renderNavigation = () => (
<>
<>
<SearchBrowseNavigation segment={this.props.segment} onChange={this.refocusOnInputSearch} />
<SearchBrowseNavigation segment={this.props.segment} onChange={this.refocusOnInputSearch} />
Copy
Copied
Copy
Copied
<FilterNavigation segment={this.props.segment} onChange={this.refocusOnInputSearch} />
</>
</>
);
);
Copy
Copied
Copy
Copied
generateInTransitItemReport = async () => {
const {
intl: {
formatMessage,
},
parentMutator,
} = this.props;
const { sendCallout, removeCallout } = this.context;
const calloutId = sendCallout({
message: <FormattedMessage id="ui-inventory.exportInProgress" />,
timeout: 0,
});
try {
const report = new InTransitItemsReport(parentMutator, formatMessage);
const items = await report.toCSV();
if (!items?.length) {
this.setState({ showErrorModal: true });
}
} catch (error) {
throw new Error(error);
} finally {
removeCallout(calloutId);
this.setState({ inTransitItemsExportInProgress: false });
}
};
startInTransitReportGeneration = () => {
const { inTransitItemsExportInProgress } = this.state;
if (inTransitItemsExportInProgress) {
return;
}
this.setState({ inTransitItemsExportInProgress: true }, this.generateInTransitItemReport);
};
generateInstancesIdReport = async (sendCallout) => {
const { instancesIdExportInProgress } = this.state;
if (instancesIdExportInProgress) return;
this.setState({ instancesIdExportInProgress: true }, async () => {
const {
reset,
GET,
} = this.props.parentMutator.recordsToExportIDs;
let infoCalloutTimer;
try {
reset();
infoCalloutTimer = setTimeout(() => {
sendCallout({
type: 'info',
message: <FormattedMessage id="ui-inventory.saveInstancesUIIDS.info" />,
});
}, INSTANCES_ID_REPORT_TIMEOUT);
const items = await GET();
clearTimeout(infoCalloutTimer);
const report = new IdReportGenerator('SearchInstanceUUIDs');
if (!isEmpty(items)) {
report.toCSV(items, record => record.id);
}
} catch (error) {
clearTimeout(infoCalloutTimer);
sendCallout({
type: 'error',
message: <FormattedMessage id="ui-inventory.saveInstancesUIIDS.error" />,
});
} finally {
this.setState({ instancesIdExportInProgress: false });
}
});
};
triggerQuickExport = async (sendCallout) => {
const { instancesQuickExportInProgress } = this.state;
if (instancesQuickExportInProgress) return;
this.setState({ instancesQuickExportInProgress: true });
try {
const instanceIds = Object.keys(this.state.selectedRows).slice(0, QUICK_EXPORT_LIMIT);
await this.props.parentMutator.quickExport.POST({
uuids: instanceIds,
type: 'uuid',
recordType: 'INSTANCE'
});
new IdReportGenerator('QuickInstanceExport').toCSV(instanceIds);
} catch (error) {
sendCallout({
type: 'error',
message: <FormattedMessage id="ui-inventory.communicationProblem" />,
});
} finally {
this.setState({ instancesQuickExportInProgress: false });
}
};
generateCQLQueryReport = async () => {
if (!isTestEnv()) {
const { data } = this.props;
const query = buildQuery(data.query, {}, data, { log: noop }, this.props);
const fileName = `SearchInstanceCQLQuery${moment().format()}.cql`;
saveAs(new Blob([query], { type: 'text/plain;charset=utf-8;' }), fileName);
Text moved from lines 145-147
}
}
toggleNewFastAddModal = () => {
this.setState((state) => {
return { showNewFastAddModal: !state.showNewFastAddModal };
});
}
generateHoldingsIdReport = async (sendCallout) => {
const { holdingsIdExportInProgress } = this.state;
if (holdingsIdExportInProgress) return;
this.setState({ holdingsIdExportInProgress: true }, async () => {
const {
reset,
GET,
} = this.props.parentMutator.holdingsToExportIDs;
let infoCalloutTimer;
try {
reset();
infoCalloutTimer = setTimeout(() => {
sendCallout({
type: 'info',
message: <FormattedMessage id="ui-inventory.saveHoldingsUIIDS.info" />,
});
}, INSTANCES_ID_REPORT_TIMEOUT);
const items = await GET();
clearTimeout(infoCalloutTimer);
const report = new IdReportGenerator('SearchHoldingsUUIDs');
if (!isEmpty(items)) {
report.toCSV(items, record => record.id);
}
} catch (error) {
clearTimeout(infoCalloutTimer);
sendCallout({
type: 'error',
message: <FormattedMessage id="ui-inventory.saveHoldingsUIIDS.error" />,
});
} finally {
this.setState({ instancesIdExportInProgress: false });
}
});
};
getActionItem = ({ id, icon, messageId, onClickHandler, isDisabled = false }) => {
return (
<Button
buttonStyle="dropdownItem"
id={id}
disabled={isDisabled}
onClick={onClickHandler}
>
<Icon
icon={icon}
size="medium"
iconClassName={css.actionIcon}
/>
<FormattedMessage id={messageId} />
</Button>
);
}
handleToggleColumn = ({ target: { value: key } }) => {
this.setState(({ visibleColumns }) => ({
visibleColumns: visibleColumns.includes(key) ? visibleColumns.filter(k => key !== k) : [...visibleColumns, key]
}), () => {
setItem(VISIBLE_COLUMNS_STORAGE_KEY, this.state.visibleColumns);
});
}
onMarkPosition = (position) => {
onMarkPosition = (position) => {
const { namespace } = this.props;
const { namespace } = this.props;
setItem(`${namespace}.position`, position);
setItem(`${namespace}.position`, position);
}
}
resetMarkedPosition = () => {
resetMarkedPosition = () => {
const { namespace } = this.props;
const { namespace } = this.props;
setItem(`${namespace}.position`, null);
setItem(`${namespace}.position`, null);
}
}
Copy
Copied
Copy
Copied
getActionMenu = ({ onToggle }) => {
const { parentResources, intl, segment } = this.props;
const { inTransitItemsExportInProgress } = this.state;
const selectedRowsCount = size(this.state.selectedRows);
const isInstancesListEmpty = isEmpty(get(parentResources, ['records', 'records'], []));
const isQuickExportLimitExceeded = selectedRowsCount > QUICK_EXPORT_LIMIT;
const visibleColumns = this.getVisibleColumns();
const columnMapping = this.getColumnMapping();
const buildOnClickHandler = onClickHandler => {
return () => {
onToggle();
onClickHandler(this.context.sendCallout);
};
};
return (
<>
<MenuSection label={intl.formatMessage({ id: 'ui-inventory.actions' })} id="actions-menu-section">
<IfPermission perm="ui-inventory.instance.create">
<Button
buttonStyle="dropdownItem"
id="clickable-newinventory"
onClick={buildOnClickHandler(this.openCreateInstance)}
>
<Icon
icon="plus-sign"
size="medium"
iconClassName={css.actionIcon}
/>
<FormattedMessage id="stripes-smart-components.new" />
</Button>
</IfPermission>
<Pluggable
id="clickable-create-inventory-records"
onClose={this.toggleNewFastAddModal}
open={this.state.showNewFastAddModal} // control the open modal via state var
renderTrigger={() => (
this.getActionItem({
id: 'new-fast-add-record',
icon: 'lightning',
messageId: 'ui-inventory.newFastAddRecord',
onClickHandler: buildOnClickHandler(this.toggleNewFastAddModal),
})
)}
type="create-inventory-records"
/>
{
inTransitItemsExportInProgress ?
this.getActionItem({
id: 'dropdown-clickable-get-report',
icon: 'report',
messageId: 'ui-inventory.exportInProgress',
}) :
this.getActionItem({
id: 'dropdown-clickable-get-report',
icon: 'report',
messageId: 'ui-inventory.inTransitReport',
onClickHandler: buildOnClickHandler(this.startInTransitReportGeneration),
})
}
{this.getActionItem({
id: 'dropdown-clickable-get-instances-uiids',
icon: 'save',
messageId: 'ui-inventory.saveInstancesUIIDS',
onClickHandler: buildOnClickHandler(this.generateInstancesIdReport),
isDisabled: isInstancesListEmpty,
})}
{segment === 'holdings' && this.getActionItem({
id: 'dropdown-clickable-get-holdings-uiids',
icon: 'save',
messageId: 'ui-inventory.saveHoldingsUIIDS',
onClickHandler: buildOnClickHandler(this.generateHoldingsIdReport),
isDisabled: isInstancesListEmpty,
})}
{this.getActionItem({
id: 'dropdown-clickable-get-cql-query',
icon: 'search',
messageId: 'ui-inventory.saveInstancesCQLQuery',
onClickHandler: buildOnClickHandler(this.generateCQLQueryReport),
isDisabled: isInstancesListEmpty,
})}
{this.getActionItem({
id: 'dropdown-clickable-export-marc',
icon: 'download',
messageId: 'ui-inventory.exportInstancesInMARC',
onClickHandler: buildOnClickHandler(this.triggerQuickExport),
isDisabled: !selectedRowsCount || isQuickExportLimitExceeded,
})}
<IfInterface name="copycat-imports">
<IfPermission perm="copycat.profiles.collection.get">
{this.getActionItem({
id: 'dropdown-clickable-import-record',
icon: 'lightning',
messageId: 'ui-inventory.copycat.import',
onClickHandler: buildOnClickHandler(() => this.setState({ isImportRecordModalOpened: true })),
})}
</IfPermission>
</IfInterface>
{isQuickExportLimitExceeded && (
<span
className={css.feedbackError}
data-test-quick-marc-export-limit-exceeded
>
<FormattedMessage
id="ui-inventory.exportInstancesInMARCLimitExceeded"
values={{ count: QUICK_EXPORT_LIMIT }}
/>
</span>
)}
{this.getActionItem({
id: 'dropdown-clickable-export-json',
icon: 'download',
messageId: 'ui-inventory.exportInstancesInJSON',
onClickHandler: buildOnClickHandler(noop),
isDisabled: true,
})}
{this.getActionItem({
id: 'dropdown-clickable-show-selected-records',
icon: 'eye-open',
messageId: 'ui-inventory.instances.showSelectedRecords',
onClickHandler: buildOnClickHandler(() => this.setState({ isSelectedRecordsModalOpened: true })),
isDisabled: !selectedRowsCount,
})}
</MenuSection>
<MenuSection label={intl.formatMessage({ id: 'ui-inventory.showColumns' })} id="columns-menu-section">
{TOGGLEABLE_COLUMNS.map(key => (
<Checkbox
key={key}
name={key}
data-testid={key}
label={columnMapping[key]}
id={`inventory-search-column-checkbox-${key}`}
checked={visibleColumns.includes(key)}
value={key}
onChange={this.handleToggleColumn}
/>
))}
</MenuSection>
</>
);
};
getColumnMapping = () => {
getColumnMapping = () => {
const { intl } = this.props;
const { intl } = this.props;
const columnMapping = {
const columnMapping = {
callNumber: intl.formatMessage({ id: 'ui-inventory.instances.columns.callNumber' }),
callNumber: intl.formatMessage({ id: 'ui-inventory.instances.columns.callNumber' }),
select: '',
select: '',
title: intl.formatMessage({ id: 'ui-inventory.instances.columns.title' }),
title: intl.formatMessage({ id: 'ui-inventory.instances.columns.title' }),
contributors: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributors' }),
contributors: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributors' }),
publishers: intl.formatMessage({ id: 'ui-inventory.instances.columns.publishers' }),
publishers: intl.formatMessage({ id: 'ui-inventory.instances.columns.publishers' }),
relation: intl.formatMessage({ id: 'ui-inventory.instances.columns.relation' }),
relation: intl.formatMessage({ id: 'ui-inventory.instances.columns.relation' }),
numberOfTitles: intl.formatMessage({ id: 'ui-inventory.instances.columns.numberOfTitles' }),
numberOfTitles: intl.formatMessage({ id: 'ui-inventory.instances.columns.numberOfTitles' }),
subject: intl.formatMessage({ id: 'ui-inventory.subject' }),
subject: intl.formatMessage({ id: 'ui-inventory.subject' }),
contributor: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributor' }),
contributor: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributor' }),
contributorType: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributorType' }),
contributorType: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributorType' }),
relatorTerm: intl.formatMessage({ id: 'ui-inventory.instances.columns.relatorTerm' }),
relatorTerm: intl.formatMessage({ id: 'ui-inventory.instances.columns.relatorTerm' }),
};
};
return columnMapping;
return columnMapping;
}
}
Copy
Copied
Copy
Copied
onErrorModalClose = () => {
this.setState({ showErrorModal: false });
};
toggleRowSelection = row => {
toggleRowSelection = row => {
this.setState(({ selectedRows }) => ({ selectedRows: getNextSelectedRowsState(selectedRows, row) }));
this.setState(({ selectedRows }) => ({ selectedRows: getNextSelectedRowsState(selectedRows, row) }));
};
};
handleResetAll = () => {
handleResetAll = () => {
this.setState({
this.setState({
selectedRows: {},
selectedRows: {},
Copy
Copied
Copy
Copied
optionSelected: '',
});
});
facetsStore.getState().resetFacetSettings();
facetsStore.getState().resetFacetSettings();
}
}
Copy
Copied
Copy
Copied
handleSelectedRecordsModalSave = selectedRecords => {
this.setState({
isSelectedRecordsModalOpened: false,
selectedRows: selectedRecords,
});
}
handleSelectedRecordsModalCancel = () => {
this.setState({ isSelectedRecordsModalOpened: false });
}
handleImportRecordModalSubmit = (args) => {
this.setState({ isImportRecordModalOpened: false });
this.props.mutator.query.update({
_path: '/inventory/import',
xidtype: args.externalIdentifierType,
xid: args.externalIdentifier,
});
}
handleImportRecordModalCancel = () => {
this.setState({ isImportRecordModalOpened: false });
}
renderPaneSub() {
renderPaneSub() {
const selectedRowsCount = size(this.state.selectedRows);
const selectedRowsCount = size(this.state.selectedRows);
return selectedRowsCount
return selectedRowsCount
? (
? (
<FormattedMessage
<FormattedMessage
id="ui-inventory.instances.rows.recordsSelected"
id="ui-inventory.instances.rows.recordsSelected"
values={{ count: selectedRowsCount }}
values={{ count: selectedRowsCount }}
/>
/>
)
)
: null;
: null;
}
}
formatCellStyles(defaultCellStyle) {
formatCellStyles(defaultCellStyle) {
return `${defaultCellStyle} ${css.cellAlign}`;
return `${defaultCellStyle} ${css.cellAlign}`;
}
}
Copy
Copied
Copy
Copied
// handler used for clicking a row in browse mode
onSelectRow = (_, row) => {
const {
parentMutator,
parentResources,
} = this.props;
switch (get(parentResources.query, 'qindex')) {
case browseModeOptions.CALL_NUMBERS:
parentMutator.query.update({
qindex: 'callNumber',
query: row.shelfKey,
filters: '',
});
break;
case browseModeOptions.SUBJECTS:
parentMutator.query.update({
qindex: 'subject',
query: row.subject,
filters: '',
});
break;
case browseModeOptions.CONTRIBUTORS:
if (row.isAnchor && !row.contributorNameTypeId) {
return;
}
parentMutator.query.update({
qindex: 'contributor',
query: row.name,
filters: `${FACETS.SEARCH_CONTRIBUTORS}.${row.contributorNameTypeId}`,
});
break;
default:
}
// the searchAndSortKey state field can be updated to reset SearchAndSort
// to use the app-level selectedIndex
this.setState((curState) => ({
searchAndSortKey: curState.searchAndSortKey + 1,
}));
}
render() {
render() {
const {
const {
Copy
Copied
Copy
Copied
browseOnly,
disableRecordCreation,
disableRecordCreation,
intl,
intl,
data,
data,
parentResources,
parentResources,
parentMutator,
parentMutator,
renderFilters,
renderFilters,
searchableIndexes,
searchableIndexes,
match: {
match: {
path,
path,
},
},
namespace,
namespace,
stripes,
stripes,
Copy
Copied
Copy
Copied
fetchFacets,
} = this.props;
} = this.props;
const {
const {
Copy
Copied
Copy
Copied
isSelectedRecordsModalOpened,
isImportRecordModalOpened,
selectedRows,
selectedRows,
Copy
Copied
Copy
Copied
optionSelected,
searchAndSortKey,
searchAndSortKey,
isSingleResult
isSingleResult
} = this.state;
} = this.state;
const itemToView = getItem(`${namespace}.position`);
const itemToView = getItem(`${namespace}.position`);
Copy
Copied
Copy
Copied
const missedMatchItem = () => {
const query = new URLSearchParams(this.props.location.search).get('query');
return (
<div className={css.missedMatchItemWrapper}>
<span className={css.warnIcon}>
<Icon
size="medium"
icon="exclamation-circle"
status="warn"
/>
</span>
<span className={`${css.missingMatchError} ${css.fitContent}`}>
{query}
</span>
<strong className={css.fitContent}>
<FormattedMessage id="ui-inventory.browseCallNumbers.missedMatch" />
</strong>
</div>
);
};
const getFullMatchRecord = (item, isAnchor) => {
const getFullMatchRecord = (item, isAnchor) => {
if (isAnchor) {
if (isAnchor) {
return <strong>{item}</strong>;
return <strong>{item}</strong>;
}
}
return item;
return item;
};
};
Copy
Copied
Copy
Copied
const handleOnNeedMore = ({ direction, records, source }) => {
const paramByBrowseMode = {
[browseModeOptions.SUBJECTS]: 'subject',
[browseModeOptions.CALL_NUMBERS]: 'callNumber',
[browseModeOptions.CONTRIBUTORS]: 'name',
};
const isSubject = optionSelected === browseModeOptions.SUBJECTS;
const isCallNumber = optionSelected === browseModeOptions.CALL_NUMBERS;
const isContributors = optionSelected === browseModeOptions.CONTRIBUTORS;
const param = paramByBrowseMode[optionSelected];
let anchor;
if (direction === 'prev') {
if (isCallNumber) {
anchor = records.find(i => i.fullCallNumber)?.shelfKey;
} else if (isSubject) {
anchor = records[0].subject;
} else if (isContributors) {
anchor = records[0].name;
}
source.fetchByQuery(`${param} < "${anchor}"`);
} else {
if (isCallNumber) {
anchor = [...records].reverse().find(i => i.fullCallNumber)?.shelfKey;
} else if (isSubject) {
anchor = records[records.length - 1].subject;
} else if (isContributors) {
anchor = records[records.length - 1].name;
}
source.fetchByQuery(`${param} > "${anchor}"`);
}
};
const resultsFormatter = {
const resultsFormatter = {
Copy
Copied
Copy
Copied
'select': ({
id,
...rowData
}) => (
<CheckboxColumn>
<Checkbox
checked={Boolean(selectedRows[id])}
aria-label={intl.formatMessage({ id: 'ui-inventory.instances.rows.select' })}
onChange={() => this.toggleRowSelection({
id,
...rowData
})}
/>
</CheckboxColumn>
),
'title': ({
'title': ({
title,
title,
Copy
Copied
Copy
Copied
instance,
discoverySuppress,
discoverySuppress,
isBoundWith,
isBoundWith,
staffSuppress,
staffSuppress,
Copy
Copied
Copy
Copied
isAnchor,
}) => {
}) => {
Copy
Copied
Copy
Copied
if (this.getExecutedBrowseQuery()) {
return (
return getFullMatchRecord(instance?.title, isAnchor);
<AppIcon
} else {
size="small"
return (
app="inventory"
iconKey="instance"
iconAlignment="baseline"
>
{title}
{(isBoundWith) &&
<AppIcon
<AppIcon
size="small"
size="small"
Copy
Copied
Copy
Copied
app="inventory"
app="@folio/inventory"
iconKey="instance"
iconKey="bound-with"
iconAlignment="baseline"
iconClassName={css.boundWithIcon}
>
/>
{title}
}
{(isBoundWith) &&
{(discoverySuppress || staffSuppress) &&
<AppIcon
<span className={css.warnIcon}>
size="small"
<Icon
app="@folio/inventory"
size="medium"
iconKey="bound-with"
icon="exclamation-circle"
iconClassName={css.boundWithIcon}
status="warn"
/>
/>
Copy
Copied
Copy
Copied
}
</span>
{(discoverySuppress || staffSuppress) &&
<span className={css.warnIcon}>
<Icon
size="medium"
icon="exclamation-circle"
status="warn"
/>
</span>
}
</AppIcon>
);
}
},
'subject': r => {
if (r?.totalRecords) {
return getFullMatchRecord(r?.subject, r.isAnchor);
}
return missedMatchItem();
},
'callNumber': r => {
if (r?.instance || r?.totalRecords) {
return getFullMatchRecord(r?.fullCallNumber, r.isAnchor);
}
return missedMatchItem();
},
'contributor': r => {
if (r?.totalRecords) {
return getFullMatchRecord(r?.name, r.isAnchor);
}
}
Copy
Copied
Copy
Copied
return missedMatchItem(
);
</AppIcon>
);
},
},
Copy
Copied
Copy
Copied
'relation': r => formatters.relationsFormatter(r, data.instanceRelationshipTypes),
'publishers': r => (r?.publication ?? []).map(p => (p ? `${p.publisher} ${p.dateOfPublication ? `(${p.dateOfPublication})` : ''}` : '')).join(', '),
'publication date': r => r.publication.map(p => p.dateOfPublication).join(', '),
'contributors': r => formatters.contributorsFormatter(r, data.contributorTypes),
'contributorType': r => data.contributorNameTypes.find(nameType => nameType.id === r.contributorNameTypeId)?.name || '',
'contributorType': r => data.contributorNameTypes.find(nameType => nameType.id === r.contributorNameTypeId)?.name || '',
'relatorTerm': r => {
'relatorTerm': r => {
if (!r.contributorTypeId) {
if (!r.contributorTypeId) {
return '';
return '';
}
}
return r.contributorTypeId.reduce((acc, contributorTypeId) => {
return r.contributorTypeId.reduce((acc, contributorTypeId) => {
return [...acc, data.contributorTypes.find(type => type.id === contributorTypeId)?.name || ''];
return [...acc, data.contributorTypes.find(type => type.id === contributorTypeId)?.name || ''];
}, []).filter(name => !!name).join(', ');
}, []).filter(name => !!name).join(', ');
},
},
'numberOfTitles': r => ((r?.instance || r?.totalRecords) || (r?.subject && r?.totalRecords > 0)) && getFullMatchRecord(r?.totalRecords, r.isAnchor),
'numberOfTitles': r => ((r?.instance || r?.totalRecords) || (r?.subject && r?.totalRecords > 0)) && getFullMatchRecord(r?.totalRecords, r.isAnchor),
};
};
Copy
Copied
Copy
Copied
const browseQueryExecuted = Boolean(this.getExecutedBrowseQuery());
const visibleColumns = this.getVisibleColumns();
const visibleColumns = this.getVisibleColumns();
const columnMapping = this.getColumnMapping();
const columnMapping = this.getColumnMapping();
Copy
Copied
Copy
Copied
const isHandleOnNeedMore = Object.values(browseModeOptions).includes(optionSelected) ? handleOnNeedMore : null;
const onChangeIndex = (e) => {
const onChangeIndex = (e) => {
Copy
Copied
Copy
Copied
this.setState({ optionSelected: e.target.value });
const isBrowseOption = Object.values(browseModeOptions).includes(e.target.value);
parentMutator.query.update({ qindex: e.target.value, filters: '' });
parentMutator.query.update({ qindex: e.target.value, filters: '' });
Copy
Copied
Copy
Copied
if (isBrowseOption) {
parentMutator.browseModeRecords.reset();
this.setState({ isSingleResult: false });
} else {
this.setState({ isSingleResult: true });
}
};
};
const browseFilter = () => {
const browseFilter = () => {
Copy
Copied
Copy
Copied
const { renderer } = getFilterConfig('instances
Browse
');
const { renderer } = getFilterConfig('instances
Search
');
if (optionSelected === browseModeOptions.SUBJECTS) {
return renderFilters;
return renderer;
} else if (optionSelected === browseModeOptions.CALL_NUMBERS) {
return renderer({
...data,
onFetchFacets: fetchFacets(data),
parentResources,
browseType: browseModeOptions.CALL_NUMBERS,
});
} else if (optionSelected === browseModeOptions.CONTRIBUTORS) {
return renderer({
...data,
onFetchFacets: fetchFacets(data),
parentResources,
browseType: browseModeOptions.CONTRIBUTORS,
});
} else
return renderFilters;
};
};
Copy
Copied
Copy
Copied
const browseSelectedString = Object.values(browseModeOptions).some(el => optionSelected.includes(el));
const customPaneSubTextBrowse = browseSelectedString ? <FormattedMessage id="ui-inventory.title.subTitle.browseCall" /> : null;
const searchFieldButtonLabelBrowse = browseSelectedString ? <FormattedMessage id="ui-inventory.browse" /> : null;
const titleBrowse = browseSelectedString ? <FormattedMessage id="ui-inventory.title.browseCall" /> : null;
const notLoadedMessageBrowse = browseSelectedString ? <FormattedMessage id="ui-inventory.notLoadedMessage.browseCall" /> : null;
const regExp = /((callNumber|subject|name) [<|>])|"*/ig;
const formattedSearchableIndexes = searchableIndexes.map(index => {
const formattedSearchableIndexes = searchableIndexes.map(index => {
const { prefix = '' } = index;
const { prefix = '' } = index;
let label = index.label;
let label = index.label;
if (index.label.includes('ui-inventory')) {
if (index.label.includes('ui-inventory')) {
label = prefix + intl.formatMessage({ id: index.label });
label = prefix + intl.formatMessage({ id: index.label });
}
}
Copy
Copied
Copy
Copied
return { ...index, label };
});
const shortcuts = [
{
name: 'new',
handler: handleKeyCommand(() => {
if (stripes.hasPerm('ui-inventory.instance.create')) {
this.openCreateInstance();
}
}),
},
];
const other = parentResources.records.other;
const pagingCanGoNext = browseQueryExecuted ? !!other?.next : null;
const pagingCanGoPrevious = browseQueryExecuted ? !!other?.prev : null;
return (
<HasCommand
commands={shortcuts}
isWithinScope={checkScope}
scope={document.body}
>
<div data-test-inventory-instances>
<SearchAndSort
key={searchAndSortKey}
packageInfo={packageInfo}
objectName="inventory"
title={titleBrowse}
customPaneSubText={customPaneSubTextBrowse}
searchFieldButtonLabel={searchFieldButtonLabelBrowse}
maxSortKeys={1}
notLoadedMessage={notLoadedMessageBrowse}
renderNavigation={this.renderNavigation}
searchableIndexes={formattedSearchableIndexes}
selectedIndex={get(data.query, 'qindex')}
searchableIndexesPlaceholder={null}
initialResultCount={INITIAL_RESULT_COUNT}
resultCountIncrement={RESULT_COUNT_INCREMENT}
isCountHidden={browseSelectedString}
viewRecordComponent={ViewInstanceWrapper}
editRecordComponent={InstanceForm}
onChangeIndex={onChangeIndex}
regExpForQuery={regExp}
newRecordInitialValues={(this.state && this.state.copiedInstance) ? this.state.copiedInstance : {
discoverySuppress: false,
staffSuppress: false,
previouslyHeld: false,
source: 'FOLIO',
}}
visibleColumns={visibleColumns}
columnMapping={columnMapping}
columnWidths={{
callNumber: '15%',
subject: '50%',
contributor: '50%',
numberOfTitles: '15%',
select: '30px',
title: '40%',
contributorType: '15%',
relatorTerm: '15%',
}}
getCellClass={this.formatCellStyles}
customPaneSub={this.renderPaneSub()}
resultsFormatter={resultsFormatter}
onCreate={this.onCreate}
viewRecordPerms="ui-inventory.instance.view"
newRecordPerms="ui-inventory.instance.create"
disableRecordCreation={disableRecordCreation || false}
parentResources={parentResources}
parentMutator={parentMutator}
detailProps={{
referenceTables: data,
onCopy: this.copyInstance,
}}
basePath={path}
path={`${path}/(view|viewsource)/:id/:holdingsrecordid?/:itemid?`}
showSingleResult={isSingleResult}
browseOnly={browseOnly}
onSelectRow={browseQueryExecuted ? this.onSelectRow : undefined}
renderFilters={browseFilter()}
onFilterChange={this.onFilterChangeHandler}
pageAmount={100}
pagingType={pagingTypes.PREV_NEXT}
hidePageIndices={browseQueryExecuted}
hasNewButton={false}
onResetAll={this.handleResetAll}
sortableColumns={['title', 'contributors', 'publishers']}
syncQueryWithUrl
resultsVirtualize={false}
resultsOnMarkPosition={this.onMarkPosition}
resultsOnResetMarkedPosition={this.resetMarkedPosition}
resultsCachedPosition={itemToView}
resultsOnNeedMore={isHandleOnNeedMore}
pagingCanGoNext={pagingCanGoNext}
pagingCanGoPrevious={pagingCanGoPrevious}
/>
</div>
</HasCommand>
);
}
}
export default withNamespace(flowRight(
injectIntl,
withLocation,
)(stripesConnect(InstancesList)));
Saved diffs
Original text
Open file
import React from 'react'; import PropTypes from 'prop-types'; import { omit, get, set, flowRight, size, } from 'lodash'; import { injectIntl, FormattedMessage, } from 'react-intl'; import { AppIcon, CalloutContext, stripesConnect, withNamespace, } from '@folio/stripes/core'; import { SearchAndSort } from '@folio/stripes/smart-components'; import { Icon, Checkbox, checkScope, HasCommand, } from '@folio/stripes/components'; import { pagingTypes } from '@folio/stripes-components/lib/MultiColumnList'; import packageInfo from '../../../package'; import InstanceForm from '../../edit/InstanceForm'; import ViewInstanceWrapper from '../../ViewInstanceWrapper'; import formatters from '../../referenceFormatters'; import withLocation from '../../withLocation'; import CheckboxColumn from '../../components/InstancesList/CheckboxColumn'; import { getCurrentFilters, getNextSelectedRowsState, parseFiltersToStr, marshalInstance, omitFromArray, handleKeyCommand, } from '../../utils'; import { segments, browseModeOptions, FACETS, } from '../../constants'; import { getItem, setItem, } from '../../storage'; import facetsStore from '../../stores/facetsStore'; import css from '../../components/InstancesList/instances.css'; import { getFilterConfig } from '../../filterConfig'; import SearchBrowseNavigation from '../../components/SearchBrowseNavigation/SearchBrowseNavigation'; const INITIAL_RESULT_COUNT = 30; const RESULT_COUNT_INCREMENT = 30; const columnSets = { SUBJECTS: ['subject', 'numberOfTitles'], CALL_NUMBERS: ['callNumber', 'title', 'numberOfTitles'], CONTRIBUTORS: ['contributor', 'contributorType', 'relatorTerm', 'numberOfTitles'], }; const TOGGLEABLE_COLUMNS = ['contributors', 'publishers', 'relation']; const NON_TOGGLEABLE_COLUMNS = ['select', 'title']; const ALL_COLUMNS = Array.from(new Set([ ...NON_TOGGLEABLE_COLUMNS, ...TOGGLEABLE_COLUMNS, ...columnSets.CALL_NUMBERS, ...columnSets.SUBJECTS, ...columnSets.CONTRIBUTORS, ])); const VISIBLE_COLUMNS_STORAGE_KEY = 'inventory-visible-columns'; class InstancesList extends React.Component { static defaultProps = { browseOnly: false, showSingleResult: true, }; static propTypes = { data: PropTypes.object, parentResources: PropTypes.object, parentMutator: PropTypes.object, showSingleResult: PropTypes.bool, browseOnly: PropTypes.bool, disableRecordCreation: PropTypes.bool, updateLocation: PropTypes.func.isRequired, goTo: PropTypes.func.isRequired, getParams: PropTypes.func.isRequired, segment: PropTypes.string, intl: PropTypes.object, match: PropTypes.shape({ path: PropTypes.string.isRequired, params: PropTypes.object.isRequired, }).isRequired, namespace: PropTypes.string, renderFilters: PropTypes.func.isRequired, searchableIndexes: PropTypes.arrayOf(PropTypes.object).isRequired, mutator: PropTypes.shape({ query: PropTypes.shape({ update: PropTypes.func.isRequired, replace: PropTypes.func.isRequired, }).isRequired, }), location: PropTypes.shape({ search: PropTypes.string }), stripes: PropTypes.object.isRequired, fetchFacets: PropTypes.func, }; static contextType = CalloutContext; static manifest = Object.freeze({ query: {}, }); constructor(props) { super(props); this.state = { selectedRows: {}, optionSelected: '', searchAndSortKey: 0, isSingleResult: this.props.showSingleResult, }; } componentDidMount() { if (this.getSelectedBrowseOption() || this.getExecutedBrowseQuery()) { this.setState({ optionSelected: this.getQIndexFromParams(), isSingleResult: false, }); } else { this.setState({ optionSelected: '', }); } } getQIndexFromParams = () => { const params = new URLSearchParams(this.props.location.search); return params.get('qindex'); } getSelectedBrowseOption = () => { const isBrowseSelectedBasedOnState = Object.keys(browseModeOptions).filter(k => browseModeOptions[k] === this.state.optionSelected)[0]; return isBrowseSelectedBasedOnState; } getExecutedBrowseQuery = () => { const isBrowseSelectedBasedOnUrl = Object.keys(browseModeOptions).filter(k => browseModeOptions[k] === this.getQIndexFromParams())[0]; return isBrowseSelectedBasedOnUrl; } getInitialToggableColumns = () => { return getItem(VISIBLE_COLUMNS_STORAGE_KEY) || TOGGLEABLE_COLUMNS; } getVisibleColumns = () => { // display particular columnset based on the quindex value in the URL, since the search has been // performed. const executedBrowseQuery = Object.keys(browseModeOptions).filter(k => browseModeOptions[k] === this.getQIndexFromParams())[0]; const columns = columnSets[executedBrowseQuery]; if (!executedBrowseQuery) { const visibleColumns = new Set([...NON_TOGGLEABLE_COLUMNS]); return ALL_COLUMNS.filter(key => visibleColumns.has(key)); } else { const visibleColumns = new Set([...columns]); return Array.from(visibleColumns); } } onFilterChangeHandler = ({ name, values }) => { const { data: { query }, match: { path }, goTo, getParams, } = this.props; const curFilters = getCurrentFilters(get(query, 'filters', '')); const mergedFilters = values.length ? { ...curFilters, [name]: values } : omit(curFilters, name); const filtersStr = parseFiltersToStr(mergedFilters); const params = getParams(); goTo(path, { ...params, filters: filtersStr }); }; closeNewInstance = (e) => { if (e) e.preventDefault(); this.setState({ copiedInstance: null }); this.props.updateLocation({ layer: null }); }; createInstance = (instanceData) => { const { data: { identifierTypesByName } } = this.props; // Massage record to add preceeding and succeeding title fields const instance = marshalInstance(instanceData, identifierTypesByName); // POST item record return this.props.parentMutator.records.POST(instance); }; onCreate = (instance) => { return this.createInstance(instance).then(() => this.closeNewInstance()); } openCreateInstance = () => { this.props.updateLocation({ layer: 'create' }); } copyInstance = (instance) => { const { precedingTitles, succeedingTitles, childInstances, parentInstances, } = instance; let copiedInstance = omit(instance, ['id', 'hrid']); if (precedingTitles?.length) { copiedInstance.precedingTitles = omitFromArray(precedingTitles, 'id'); } if (succeedingTitles?.length) { copiedInstance.succeedingTitles = omitFromArray(succeedingTitles, 'id'); } if (childInstances?.length) { copiedInstance.childInstances = omitFromArray(childInstances, 'id'); } if (parentInstances?.length) { copiedInstance.parentInstances = omitFromArray(parentInstances, 'id'); } copiedInstance = set(copiedInstance, 'source', 'FOLIO'); this.setState({ copiedInstance }); this.openCreateInstance(); } refocusOnInputSearch = (segment) => { // when navigation button is clicked to change the search segment // the focus stays on the button so refocus back on the input search. // https://issues.folio.org/browse/UIIN-1358 if (segment !== segments.instances) { this.setState({ optionSelected: '' }); } facetsStore.getState().resetFacetSettings(); document.getElementById('input-inventory-search').focus(); } renderNavigation = () => ( <> <SearchBrowseNavigation segment={this.props.segment} onChange={this.refocusOnInputSearch} /> </> ); onMarkPosition = (position) => { const { namespace } = this.props; setItem(`${namespace}.position`, position); } resetMarkedPosition = () => { const { namespace } = this.props; setItem(`${namespace}.position`, null); } getColumnMapping = () => { const { intl } = this.props; const columnMapping = { callNumber: intl.formatMessage({ id: 'ui-inventory.instances.columns.callNumber' }), select: '', title: intl.formatMessage({ id: 'ui-inventory.instances.columns.title' }), contributors: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributors' }), publishers: intl.formatMessage({ id: 'ui-inventory.instances.columns.publishers' }), relation: intl.formatMessage({ id: 'ui-inventory.instances.columns.relation' }), numberOfTitles: intl.formatMessage({ id: 'ui-inventory.instances.columns.numberOfTitles' }), subject: intl.formatMessage({ id: 'ui-inventory.subject' }), contributor: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributor' }), contributorType: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributorType' }), relatorTerm: intl.formatMessage({ id: 'ui-inventory.instances.columns.relatorTerm' }), }; return columnMapping; } toggleRowSelection = row => { this.setState(({ selectedRows }) => ({ selectedRows: getNextSelectedRowsState(selectedRows, row) })); }; handleResetAll = () => { this.setState({ selectedRows: {}, optionSelected: '', }); facetsStore.getState().resetFacetSettings(); } renderPaneSub() { const selectedRowsCount = size(this.state.selectedRows); return selectedRowsCount ? ( <FormattedMessage id="ui-inventory.instances.rows.recordsSelected" values={{ count: selectedRowsCount }} /> ) : null; } formatCellStyles(defaultCellStyle) { return `${defaultCellStyle} ${css.cellAlign}`; } // handler used for clicking a row in browse mode onSelectRow = (_, row) => { const { parentMutator, parentResources, } = this.props; switch (get(parentResources.query, 'qindex')) { case browseModeOptions.CALL_NUMBERS: parentMutator.query.update({ qindex: 'callNumber', query: row.shelfKey, filters: '', }); break; case browseModeOptions.SUBJECTS: parentMutator.query.update({ qindex: 'subject', query: row.subject, filters: '', }); break; case browseModeOptions.CONTRIBUTORS: if (row.isAnchor && !row.contributorNameTypeId) { return; } parentMutator.query.update({ qindex: 'contributor', query: row.name, filters: `${FACETS.SEARCH_CONTRIBUTORS}.${row.contributorNameTypeId}`, }); break; default: } // the searchAndSortKey state field can be updated to reset SearchAndSort // to use the app-level selectedIndex this.setState((curState) => ({ searchAndSortKey: curState.searchAndSortKey + 1, })); } render() { const { browseOnly, disableRecordCreation, intl, data, parentResources, parentMutator, renderFilters, searchableIndexes, match: { path, }, namespace, stripes, fetchFacets, } = this.props; const { selectedRows, optionSelected, searchAndSortKey, isSingleResult } = this.state; const itemToView = getItem(`${namespace}.position`); const missedMatchItem = () => { const query = new URLSearchParams(this.props.location.search).get('query'); return ( <div className={css.missedMatchItemWrapper}> <span className={css.warnIcon}> <Icon size="medium" icon="exclamation-circle" status="warn" /> </span> <span className={`${css.missingMatchError} ${css.fitContent}`}> {query} </span> <strong className={css.fitContent}> <FormattedMessage id="ui-inventory.browseCallNumbers.missedMatch" /> </strong> </div> ); }; const getFullMatchRecord = (item, isAnchor) => { if (isAnchor) { return <strong>{item}</strong>; } return item; }; const handleOnNeedMore = ({ direction, records, source }) => { const paramByBrowseMode = { [browseModeOptions.SUBJECTS]: 'subject', [browseModeOptions.CALL_NUMBERS]: 'callNumber', [browseModeOptions.CONTRIBUTORS]: 'name', }; const isSubject = optionSelected === browseModeOptions.SUBJECTS; const isCallNumber = optionSelected === browseModeOptions.CALL_NUMBERS; const isContributors = optionSelected === browseModeOptions.CONTRIBUTORS; const param = paramByBrowseMode[optionSelected]; let anchor; if (direction === 'prev') { if (isCallNumber) { anchor = records.find(i => i.fullCallNumber)?.shelfKey; } else if (isSubject) { anchor = records[0].subject; } else if (isContributors) { anchor = records[0].name; } source.fetchByQuery(`${param} < "${anchor}"`); } else { if (isCallNumber) { anchor = [...records].reverse().find(i => i.fullCallNumber)?.shelfKey; } else if (isSubject) { anchor = records[records.length - 1].subject; } else if (isContributors) { anchor = records[records.length - 1].name; } source.fetchByQuery(`${param} > "${anchor}"`); } }; const resultsFormatter = { 'title': ({ title, instance, discoverySuppress, isBoundWith, staffSuppress, isAnchor, }) => { if (this.getExecutedBrowseQuery()) { return getFullMatchRecord(instance?.title, isAnchor); } else { return ( <AppIcon size="small" app="inventory" iconKey="instance" iconAlignment="baseline" > {title} {(isBoundWith) && <AppIcon size="small" app="@folio/inventory" iconKey="bound-with" iconClassName={css.boundWithIcon} /> } {(discoverySuppress || staffSuppress) && <span className={css.warnIcon}> <Icon size="medium" icon="exclamation-circle" status="warn" /> </span> } </AppIcon> ); } }, 'subject': r => { if (r?.totalRecords) { return getFullMatchRecord(r?.subject, r.isAnchor); } return missedMatchItem(); }, 'callNumber': r => { if (r?.instance || r?.totalRecords) { return getFullMatchRecord(r?.fullCallNumber, r.isAnchor); } return missedMatchItem(); }, 'contributor': r => { if (r?.totalRecords) { return getFullMatchRecord(r?.name, r.isAnchor); } return missedMatchItem(); }, 'contributorType': r => data.contributorNameTypes.find(nameType => nameType.id === r.contributorNameTypeId)?.name || '', 'relatorTerm': r => { if (!r.contributorTypeId) { return ''; } return r.contributorTypeId.reduce((acc, contributorTypeId) => { return [...acc, data.contributorTypes.find(type => type.id === contributorTypeId)?.name || '']; }, []).filter(name => !!name).join(', '); }, 'numberOfTitles': r => ((r?.instance || r?.totalRecords) || (r?.subject && r?.totalRecords > 0)) && getFullMatchRecord(r?.totalRecords, r.isAnchor), }; const browseQueryExecuted = Boolean(this.getExecutedBrowseQuery()); const visibleColumns = this.getVisibleColumns(); const columnMapping = this.getColumnMapping(); const isHandleOnNeedMore = Object.values(browseModeOptions).includes(optionSelected) ? handleOnNeedMore : null; const onChangeIndex = (e) => { this.setState({ optionSelected: e.target.value }); const isBrowseOption = Object.values(browseModeOptions).includes(e.target.value); parentMutator.query.update({ qindex: e.target.value, filters: '' }); if (isBrowseOption) { parentMutator.browseModeRecords.reset(); this.setState({ isSingleResult: false }); } else { this.setState({ isSingleResult: true }); } }; const browseFilter = () => { const { renderer } = getFilterConfig('instancesBrowse'); if (optionSelected === browseModeOptions.SUBJECTS) { return renderer; } else if (optionSelected === browseModeOptions.CALL_NUMBERS) { return renderer({ ...data, onFetchFacets: fetchFacets(data), parentResources, browseType: browseModeOptions.CALL_NUMBERS, }); } else if (optionSelected === browseModeOptions.CONTRIBUTORS) { return renderer({ ...data, onFetchFacets: fetchFacets(data), parentResources, browseType: browseModeOptions.CONTRIBUTORS, }); } else return renderFilters; }; const browseSelectedString = Object.values(browseModeOptions).some(el => optionSelected.includes(el)); const customPaneSubTextBrowse = browseSelectedString ? <FormattedMessage id="ui-inventory.title.subTitle.browseCall" /> : null; const searchFieldButtonLabelBrowse = browseSelectedString ? <FormattedMessage id="ui-inventory.browse" /> : null; const titleBrowse = browseSelectedString ? <FormattedMessage id="ui-inventory.title.browseCall" /> : null; const notLoadedMessageBrowse = browseSelectedString ? <FormattedMessage id="ui-inventory.notLoadedMessage.browseCall" /> : null; const regExp = /((callNumber|subject|name) [<|>])|"*/ig; const formattedSearchableIndexes = searchableIndexes.map(index => { const { prefix = '' } = index; let label = index.label; if (index.label.includes('ui-inventory')) { label = prefix + intl.formatMessage({ id: index.label }); } return { ...index, label }; }); const shortcuts = [ { name: 'new', handler: handleKeyCommand(() => { if (stripes.hasPerm('ui-inventory.instance.create')) { this.openCreateInstance(); } }), }, ]; const other = parentResources.records.other; const pagingCanGoNext = browseQueryExecuted ? !!other?.next : null; const pagingCanGoPrevious = browseQueryExecuted ? !!other?.prev : null; return ( <HasCommand commands={shortcuts} isWithinScope={checkScope} scope={document.body} > <div data-test-inventory-instances> <SearchAndSort key={searchAndSortKey} packageInfo={packageInfo} objectName="inventory" title={titleBrowse} customPaneSubText={customPaneSubTextBrowse} searchFieldButtonLabel={searchFieldButtonLabelBrowse} maxSortKeys={1} notLoadedMessage={notLoadedMessageBrowse} renderNavigation={this.renderNavigation} searchableIndexes={formattedSearchableIndexes} selectedIndex={get(data.query, 'qindex')} searchableIndexesPlaceholder={null} initialResultCount={INITIAL_RESULT_COUNT} resultCountIncrement={RESULT_COUNT_INCREMENT} isCountHidden={browseSelectedString} viewRecordComponent={ViewInstanceWrapper} editRecordComponent={InstanceForm} onChangeIndex={onChangeIndex} regExpForQuery={regExp} newRecordInitialValues={(this.state && this.state.copiedInstance) ? this.state.copiedInstance : { discoverySuppress: false, staffSuppress: false, previouslyHeld: false, source: 'FOLIO', }} visibleColumns={visibleColumns} columnMapping={columnMapping} columnWidths={{ callNumber: '15%', subject: '50%', contributor: '50%', numberOfTitles: '15%', select: '30px', title: '40%', contributorType: '15%', relatorTerm: '15%', }} getCellClass={this.formatCellStyles} customPaneSub={this.renderPaneSub()} resultsFormatter={resultsFormatter} onCreate={this.onCreate} viewRecordPerms="ui-inventory.instance.view" newRecordPerms="ui-inventory.instance.create" disableRecordCreation={disableRecordCreation || false} parentResources={parentResources} parentMutator={parentMutator} detailProps={{ referenceTables: data, onCopy: this.copyInstance, }} basePath={path} path={`${path}/(view|viewsource)/:id/:holdingsrecordid?/:itemid?`} showSingleResult={isSingleResult} browseOnly={browseOnly} onSelectRow={browseQueryExecuted ? this.onSelectRow : undefined} renderFilters={browseFilter()} onFilterChange={this.onFilterChangeHandler} pageAmount={100} pagingType={pagingTypes.PREV_NEXT} hidePageIndices={browseQueryExecuted} hasNewButton={false} onResetAll={this.handleResetAll} sortableColumns={['title', 'contributors', 'publishers']} syncQueryWithUrl resultsVirtualize={false} resultsOnMarkPosition={this.onMarkPosition} resultsOnResetMarkedPosition={this.resetMarkedPosition} resultsCachedPosition={itemToView} resultsOnNeedMore={isHandleOnNeedMore} pagingCanGoNext={pagingCanGoNext} pagingCanGoPrevious={pagingCanGoPrevious} /> </div> </HasCommand> ); } } export default withNamespace(flowRight( injectIntl, withLocation, )(stripesConnect(InstancesList)));
Changed text
Open file
import React from 'react'; import PropTypes from 'prop-types'; import { omit, get, set, flowRight, size, isEmpty, noop, } from 'lodash'; import { injectIntl, FormattedMessage, } from 'react-intl'; import saveAs from 'file-saver'; import moment from 'moment'; import { Pluggable, AppIcon, IfPermission, IfInterface, CalloutContext, stripesConnect, withNamespace, } from '@folio/stripes/core'; import { SearchAndSort } from '@folio/stripes/smart-components'; import { Button, Icon, Checkbox, MenuSection, checkScope, HasCommand, } from '@folio/stripes/components'; import { pagingTypes } from '@folio/stripes-components/lib/MultiColumnList'; import FilterNavigation from '../../components/FilterNavigation'; import packageInfo from '../../../package'; import InstanceForm from '../../edit/InstanceForm'; import ViewInstanceWrapper from '../../ViewInstanceWrapper'; import formatters from '../../referenceFormatters'; import withLocation from '../../withLocation'; import { getCurrentFilters, getNextSelectedRowsState, parseFiltersToStr, marshalInstance, omitFromArray, isTestEnv, handleKeyCommand, } from '../../utils'; import { INSTANCES_ID_REPORT_TIMEOUT, QUICK_EXPORT_LIMIT, segments, } from '../../constants'; import { IdReportGenerator, InTransitItemsReport, } from '../../reports'; import ErrorModal from '../../components/ErrorModal'; import CheckboxColumn from '../../components/InstancesList/CheckboxColumn'; import SelectedRecordsModal from '../../components/SelectedRecordsModal'; import ImportRecordModal from '../../components/ImportRecordModal'; import { buildQuery } from '../../routes/buildManifestObject'; import { getItem, setItem, } from '../../storage'; import facetsStore from '../../stores/facetsStore'; import css from '../../components/InstancesList/instances.css'; import { getFilterConfig } from '../../filterConfig'; import SearchBrowseNavigation from '../../components/SearchBrowseNavigation/SearchBrowseNavigation'; const INITIAL_RESULT_COUNT = 30; const RESULT_COUNT_INCREMENT = 30; const columnSets = { SUBJECTS: ['subject', 'numberOfTitles'], CALL_NUMBERS: ['callNumber', 'title', 'numberOfTitles'], CONTRIBUTORS: ['contributor', 'contributorType', 'relatorTerm', 'numberOfTitles'], }; const TOGGLEABLE_COLUMNS = ['contributors', 'publishers', 'relation']; const NON_TOGGLEABLE_COLUMNS = ['select', 'title']; const ALL_COLUMNS = Array.from(new Set([ ...NON_TOGGLEABLE_COLUMNS, ...TOGGLEABLE_COLUMNS, ...columnSets.CALL_NUMBERS, ...columnSets.SUBJECTS, ...columnSets.CONTRIBUTORS, ])); const VISIBLE_COLUMNS_STORAGE_KEY = 'inventory-visible-columns'; class SearchView extends React.Component { static defaultProps = { showSingleResult: true, }; static propTypes = { data: PropTypes.object, parentResources: PropTypes.object, parentMutator: PropTypes.object, showSingleResult: PropTypes.bool, disableRecordCreation: PropTypes.bool, updateLocation: PropTypes.func.isRequired, goTo: PropTypes.func.isRequired, getParams: PropTypes.func.isRequired, segment: PropTypes.string, intl: PropTypes.object, match: PropTypes.shape({ path: PropTypes.string.isRequired, params: PropTypes.object.isRequired, }).isRequired, namespace: PropTypes.string, renderFilters: PropTypes.func.isRequired, searchableIndexes: PropTypes.arrayOf(PropTypes.object).isRequired, mutator: PropTypes.shape({ query: PropTypes.shape({ update: PropTypes.func.isRequired, replace: PropTypes.func.isRequired, }).isRequired, }), location: PropTypes.shape({ search: PropTypes.string }), stripes: PropTypes.object.isRequired, fetchFacets: PropTypes.func, }; static contextType = CalloutContext; static manifest = Object.freeze({ query: {}, }); constructor(props) { super(props); this.state = { showNewFastAddModal: false, inTransitItemsExportInProgress: false, instancesIdExportInProgress: false, holdingsIdExportInProgress: false, instancesQuickExportInProgress: false, showErrorModal: false, selectedRows: {}, isSelectedRecordsModalOpened: false, visibleColumns: this.getInitialToggableColumns(), isImportRecordModalOpened: false, searchAndSortKey: 0, isSingleResult: this.props.showSingleResult, }; } getQIndexFromParams = () => { const params = new URLSearchParams(this.props.location.search); return params.get('qindex'); } getInitialToggableColumns = () => { return getItem(VISIBLE_COLUMNS_STORAGE_KEY) || TOGGLEABLE_COLUMNS; } getVisibleColumns = () => { // display particular columnset based on the quindex value in the URL, since the search has been // performed. const columns = this.state.visibleColumns; const visibleColumns = new Set([...columns, ...NON_TOGGLEABLE_COLUMNS]); return ALL_COLUMNS.filter(key => visibleColumns.has(key)); } onFilterChangeHandler = ({ name, values }) => { const { data: { query }, match: { path }, goTo, getParams, } = this.props; const curFilters = getCurrentFilters(get(query, 'filters', '')); const mergedFilters = values.length ? { ...curFilters, [name]: values } : omit(curFilters, name); const filtersStr = parseFiltersToStr(mergedFilters); const params = getParams(); goTo(path, { ...params, filters: filtersStr }); }; closeNewInstance = (e) => { if (e) e.preventDefault(); this.setState({ copiedInstance: null }); this.props.updateLocation({ layer: null }); }; createInstance = (instanceData) => { const { data: { identifierTypesByName } } = this.props; // Massage record to add preceeding and succeeding title fields const instance = marshalInstance(instanceData, identifierTypesByName); // POST item record return this.props.parentMutator.records.POST(instance); }; onCreate = (instance) => { return this.createInstance(instance).then(() => this.closeNewInstance()); } openCreateInstance = () => { this.props.updateLocation({ layer: 'create' }); } copyInstance = (instance) => { const { precedingTitles, succeedingTitles, childInstances, parentInstances, } = instance; let copiedInstance = omit(instance, ['id', 'hrid']); if (precedingTitles?.length) { copiedInstance.precedingTitles = omitFromArray(precedingTitles, 'id'); } if (succeedingTitles?.length) { copiedInstance.succeedingTitles = omitFromArray(succeedingTitles, 'id'); } if (childInstances?.length) { copiedInstance.childInstances = omitFromArray(childInstances, 'id'); } if (parentInstances?.length) { copiedInstance.parentInstances = omitFromArray(parentInstances, 'id'); } copiedInstance = set(copiedInstance, 'source', 'FOLIO'); this.setState({ copiedInstance }); this.openCreateInstance(); } refocusOnInputSearch = () => { // when navigation button is clicked to change the search segment // the focus stays on the button so refocus back on the input search. // https://issues.folio.org/browse/UIIN-1358 facetsStore.getState().resetFacetSettings(); document.getElementById('input-inventory-search').focus(); } renderNavigation = () => ( <> <SearchBrowseNavigation segment={this.props.segment} onChange={this.refocusOnInputSearch} /> <FilterNavigation segment={this.props.segment} onChange={this.refocusOnInputSearch} /> </> ); generateInTransitItemReport = async () => { const { intl: { formatMessage, }, parentMutator, } = this.props; const { sendCallout, removeCallout } = this.context; const calloutId = sendCallout({ message: <FormattedMessage id="ui-inventory.exportInProgress" />, timeout: 0, }); try { const report = new InTransitItemsReport(parentMutator, formatMessage); const items = await report.toCSV(); if (!items?.length) { this.setState({ showErrorModal: true }); } } catch (error) { throw new Error(error); } finally { removeCallout(calloutId); this.setState({ inTransitItemsExportInProgress: false }); } }; startInTransitReportGeneration = () => { const { inTransitItemsExportInProgress } = this.state; if (inTransitItemsExportInProgress) { return; } this.setState({ inTransitItemsExportInProgress: true }, this.generateInTransitItemReport); }; generateInstancesIdReport = async (sendCallout) => { const { instancesIdExportInProgress } = this.state; if (instancesIdExportInProgress) return; this.setState({ instancesIdExportInProgress: true }, async () => { const { reset, GET, } = this.props.parentMutator.recordsToExportIDs; let infoCalloutTimer; try { reset(); infoCalloutTimer = setTimeout(() => { sendCallout({ type: 'info', message: <FormattedMessage id="ui-inventory.saveInstancesUIIDS.info" />, }); }, INSTANCES_ID_REPORT_TIMEOUT); const items = await GET(); clearTimeout(infoCalloutTimer); const report = new IdReportGenerator('SearchInstanceUUIDs'); if (!isEmpty(items)) { report.toCSV(items, record => record.id); } } catch (error) { clearTimeout(infoCalloutTimer); sendCallout({ type: 'error', message: <FormattedMessage id="ui-inventory.saveInstancesUIIDS.error" />, }); } finally { this.setState({ instancesIdExportInProgress: false }); } }); }; triggerQuickExport = async (sendCallout) => { const { instancesQuickExportInProgress } = this.state; if (instancesQuickExportInProgress) return; this.setState({ instancesQuickExportInProgress: true }); try { const instanceIds = Object.keys(this.state.selectedRows).slice(0, QUICK_EXPORT_LIMIT); await this.props.parentMutator.quickExport.POST({ uuids: instanceIds, type: 'uuid', recordType: 'INSTANCE' }); new IdReportGenerator('QuickInstanceExport').toCSV(instanceIds); } catch (error) { sendCallout({ type: 'error', message: <FormattedMessage id="ui-inventory.communicationProblem" />, }); } finally { this.setState({ instancesQuickExportInProgress: false }); } }; generateCQLQueryReport = async () => { if (!isTestEnv()) { const { data } = this.props; const query = buildQuery(data.query, {}, data, { log: noop }, this.props); const fileName = `SearchInstanceCQLQuery${moment().format()}.cql`; saveAs(new Blob([query], { type: 'text/plain;charset=utf-8;' }), fileName); } } toggleNewFastAddModal = () => { this.setState((state) => { return { showNewFastAddModal: !state.showNewFastAddModal }; }); } generateHoldingsIdReport = async (sendCallout) => { const { holdingsIdExportInProgress } = this.state; if (holdingsIdExportInProgress) return; this.setState({ holdingsIdExportInProgress: true }, async () => { const { reset, GET, } = this.props.parentMutator.holdingsToExportIDs; let infoCalloutTimer; try { reset(); infoCalloutTimer = setTimeout(() => { sendCallout({ type: 'info', message: <FormattedMessage id="ui-inventory.saveHoldingsUIIDS.info" />, }); }, INSTANCES_ID_REPORT_TIMEOUT); const items = await GET(); clearTimeout(infoCalloutTimer); const report = new IdReportGenerator('SearchHoldingsUUIDs'); if (!isEmpty(items)) { report.toCSV(items, record => record.id); } } catch (error) { clearTimeout(infoCalloutTimer); sendCallout({ type: 'error', message: <FormattedMessage id="ui-inventory.saveHoldingsUIIDS.error" />, }); } finally { this.setState({ instancesIdExportInProgress: false }); } }); }; getActionItem = ({ id, icon, messageId, onClickHandler, isDisabled = false }) => { return ( <Button buttonStyle="dropdownItem" id={id} disabled={isDisabled} onClick={onClickHandler} > <Icon icon={icon} size="medium" iconClassName={css.actionIcon} /> <FormattedMessage id={messageId} /> </Button> ); } handleToggleColumn = ({ target: { value: key } }) => { this.setState(({ visibleColumns }) => ({ visibleColumns: visibleColumns.includes(key) ? visibleColumns.filter(k => key !== k) : [...visibleColumns, key] }), () => { setItem(VISIBLE_COLUMNS_STORAGE_KEY, this.state.visibleColumns); }); } onMarkPosition = (position) => { const { namespace } = this.props; setItem(`${namespace}.position`, position); } resetMarkedPosition = () => { const { namespace } = this.props; setItem(`${namespace}.position`, null); } getActionMenu = ({ onToggle }) => { const { parentResources, intl, segment } = this.props; const { inTransitItemsExportInProgress } = this.state; const selectedRowsCount = size(this.state.selectedRows); const isInstancesListEmpty = isEmpty(get(parentResources, ['records', 'records'], [])); const isQuickExportLimitExceeded = selectedRowsCount > QUICK_EXPORT_LIMIT; const visibleColumns = this.getVisibleColumns(); const columnMapping = this.getColumnMapping(); const buildOnClickHandler = onClickHandler => { return () => { onToggle(); onClickHandler(this.context.sendCallout); }; }; return ( <> <MenuSection label={intl.formatMessage({ id: 'ui-inventory.actions' })} id="actions-menu-section"> <IfPermission perm="ui-inventory.instance.create"> <Button buttonStyle="dropdownItem" id="clickable-newinventory" onClick={buildOnClickHandler(this.openCreateInstance)} > <Icon icon="plus-sign" size="medium" iconClassName={css.actionIcon} /> <FormattedMessage id="stripes-smart-components.new" /> </Button> </IfPermission> <Pluggable id="clickable-create-inventory-records" onClose={this.toggleNewFastAddModal} open={this.state.showNewFastAddModal} // control the open modal via state var renderTrigger={() => ( this.getActionItem({ id: 'new-fast-add-record', icon: 'lightning', messageId: 'ui-inventory.newFastAddRecord', onClickHandler: buildOnClickHandler(this.toggleNewFastAddModal), }) )} type="create-inventory-records" /> { inTransitItemsExportInProgress ? this.getActionItem({ id: 'dropdown-clickable-get-report', icon: 'report', messageId: 'ui-inventory.exportInProgress', }) : this.getActionItem({ id: 'dropdown-clickable-get-report', icon: 'report', messageId: 'ui-inventory.inTransitReport', onClickHandler: buildOnClickHandler(this.startInTransitReportGeneration), }) } {this.getActionItem({ id: 'dropdown-clickable-get-instances-uiids', icon: 'save', messageId: 'ui-inventory.saveInstancesUIIDS', onClickHandler: buildOnClickHandler(this.generateInstancesIdReport), isDisabled: isInstancesListEmpty, })} {segment === 'holdings' && this.getActionItem({ id: 'dropdown-clickable-get-holdings-uiids', icon: 'save', messageId: 'ui-inventory.saveHoldingsUIIDS', onClickHandler: buildOnClickHandler(this.generateHoldingsIdReport), isDisabled: isInstancesListEmpty, })} {this.getActionItem({ id: 'dropdown-clickable-get-cql-query', icon: 'search', messageId: 'ui-inventory.saveInstancesCQLQuery', onClickHandler: buildOnClickHandler(this.generateCQLQueryReport), isDisabled: isInstancesListEmpty, })} {this.getActionItem({ id: 'dropdown-clickable-export-marc', icon: 'download', messageId: 'ui-inventory.exportInstancesInMARC', onClickHandler: buildOnClickHandler(this.triggerQuickExport), isDisabled: !selectedRowsCount || isQuickExportLimitExceeded, })} <IfInterface name="copycat-imports"> <IfPermission perm="copycat.profiles.collection.get"> {this.getActionItem({ id: 'dropdown-clickable-import-record', icon: 'lightning', messageId: 'ui-inventory.copycat.import', onClickHandler: buildOnClickHandler(() => this.setState({ isImportRecordModalOpened: true })), })} </IfPermission> </IfInterface> {isQuickExportLimitExceeded && ( <span className={css.feedbackError} data-test-quick-marc-export-limit-exceeded > <FormattedMessage id="ui-inventory.exportInstancesInMARCLimitExceeded" values={{ count: QUICK_EXPORT_LIMIT }} /> </span> )} {this.getActionItem({ id: 'dropdown-clickable-export-json', icon: 'download', messageId: 'ui-inventory.exportInstancesInJSON', onClickHandler: buildOnClickHandler(noop), isDisabled: true, })} {this.getActionItem({ id: 'dropdown-clickable-show-selected-records', icon: 'eye-open', messageId: 'ui-inventory.instances.showSelectedRecords', onClickHandler: buildOnClickHandler(() => this.setState({ isSelectedRecordsModalOpened: true })), isDisabled: !selectedRowsCount, })} </MenuSection> <MenuSection label={intl.formatMessage({ id: 'ui-inventory.showColumns' })} id="columns-menu-section"> {TOGGLEABLE_COLUMNS.map(key => ( <Checkbox key={key} name={key} data-testid={key} label={columnMapping[key]} id={`inventory-search-column-checkbox-${key}`} checked={visibleColumns.includes(key)} value={key} onChange={this.handleToggleColumn} /> ))} </MenuSection> </> ); }; getColumnMapping = () => { const { intl } = this.props; const columnMapping = { callNumber: intl.formatMessage({ id: 'ui-inventory.instances.columns.callNumber' }), select: '', title: intl.formatMessage({ id: 'ui-inventory.instances.columns.title' }), contributors: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributors' }), publishers: intl.formatMessage({ id: 'ui-inventory.instances.columns.publishers' }), relation: intl.formatMessage({ id: 'ui-inventory.instances.columns.relation' }), numberOfTitles: intl.formatMessage({ id: 'ui-inventory.instances.columns.numberOfTitles' }), subject: intl.formatMessage({ id: 'ui-inventory.subject' }), contributor: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributor' }), contributorType: intl.formatMessage({ id: 'ui-inventory.instances.columns.contributorType' }), relatorTerm: intl.formatMessage({ id: 'ui-inventory.instances.columns.relatorTerm' }), }; return columnMapping; } onErrorModalClose = () => { this.setState({ showErrorModal: false }); }; toggleRowSelection = row => { this.setState(({ selectedRows }) => ({ selectedRows: getNextSelectedRowsState(selectedRows, row) })); }; handleResetAll = () => { this.setState({ selectedRows: {}, }); facetsStore.getState().resetFacetSettings(); } handleSelectedRecordsModalSave = selectedRecords => { this.setState({ isSelectedRecordsModalOpened: false, selectedRows: selectedRecords, }); } handleSelectedRecordsModalCancel = () => { this.setState({ isSelectedRecordsModalOpened: false }); } handleImportRecordModalSubmit = (args) => { this.setState({ isImportRecordModalOpened: false }); this.props.mutator.query.update({ _path: '/inventory/import', xidtype: args.externalIdentifierType, xid: args.externalIdentifier, }); } handleImportRecordModalCancel = () => { this.setState({ isImportRecordModalOpened: false }); } renderPaneSub() { const selectedRowsCount = size(this.state.selectedRows); return selectedRowsCount ? ( <FormattedMessage id="ui-inventory.instances.rows.recordsSelected" values={{ count: selectedRowsCount }} /> ) : null; } formatCellStyles(defaultCellStyle) { return `${defaultCellStyle} ${css.cellAlign}`; } render() { const { disableRecordCreation, intl, data, parentResources, parentMutator, renderFilters, searchableIndexes, match: { path, }, namespace, stripes, } = this.props; const { isSelectedRecordsModalOpened, isImportRecordModalOpened, selectedRows, searchAndSortKey, isSingleResult } = this.state; const itemToView = getItem(`${namespace}.position`); const getFullMatchRecord = (item, isAnchor) => { if (isAnchor) { return <strong>{item}</strong>; } return item; }; const resultsFormatter = { 'select': ({ id, ...rowData }) => ( <CheckboxColumn> <Checkbox checked={Boolean(selectedRows[id])} aria-label={intl.formatMessage({ id: 'ui-inventory.instances.rows.select' })} onChange={() => this.toggleRowSelection({ id, ...rowData })} /> </CheckboxColumn> ), 'title': ({ title, discoverySuppress, isBoundWith, staffSuppress, }) => { return ( <AppIcon size="small" app="inventory" iconKey="instance" iconAlignment="baseline" > {title} {(isBoundWith) && <AppIcon size="small" app="@folio/inventory" iconKey="bound-with" iconClassName={css.boundWithIcon} /> } {(discoverySuppress || staffSuppress) && <span className={css.warnIcon}> <Icon size="medium" icon="exclamation-circle" status="warn" /> </span> } </AppIcon> ); }, 'relation': r => formatters.relationsFormatter(r, data.instanceRelationshipTypes), 'publishers': r => (r?.publication ?? []).map(p => (p ? `${p.publisher} ${p.dateOfPublication ? `(${p.dateOfPublication})` : ''}` : '')).join(', '), 'publication date': r => r.publication.map(p => p.dateOfPublication).join(', '), 'contributors': r => formatters.contributorsFormatter(r, data.contributorTypes), 'contributorType': r => data.contributorNameTypes.find(nameType => nameType.id === r.contributorNameTypeId)?.name || '', 'relatorTerm': r => { if (!r.contributorTypeId) { return ''; } return r.contributorTypeId.reduce((acc, contributorTypeId) => { return [...acc, data.contributorTypes.find(type => type.id === contributorTypeId)?.name || '']; }, []).filter(name => !!name).join(', '); }, 'numberOfTitles': r => ((r?.instance || r?.totalRecords) || (r?.subject && r?.totalRecords > 0)) && getFullMatchRecord(r?.totalRecords, r.isAnchor), }; const visibleColumns = this.getVisibleColumns(); const columnMapping = this.getColumnMapping(); const onChangeIndex = (e) => { parentMutator.query.update({ qindex: e.target.value, filters: '' }); }; const browseFilter = () => { const { renderer } = getFilterConfig('instancesSearch'); return renderFilters; }; const formattedSearchableIndexes = searchableIndexes.map(index => { const { prefix = '' } = index; let label = index.label; if (index.label.includes('ui-inventory')) { label = prefix + intl.formatMessage({ id: index.label }); } return { ...index, label }; }); const shortcuts = [ { name: 'new', handler: handleKeyCommand(() => { if (stripes.hasPerm('ui-inventory.instance.create')) { this.openCreateInstance(); } }), }, ]; return ( <HasCommand commands={shortcuts} isWithinScope={checkScope} scope={document.body} > <div data-test-inventory-instances> <SearchAndSort key={searchAndSortKey} actionMenu={this.getActionMenu} packageInfo={packageInfo} objectName="inventory" maxSortKeys={1} renderNavigation={this.renderNavigation} searchableIndexes={formattedSearchableIndexes} selectedIndex={get(data.query, 'qindex')} searchableIndexesPlaceholder={null} initialResultCount={INITIAL_RESULT_COUNT} resultCountIncrement={RESULT_COUNT_INCREMENT} viewRecordComponent={ViewInstanceWrapper} editRecordComponent={InstanceForm} onChangeIndex={onChangeIndex} newRecordInitialValues={(this.state && this.state.copiedInstance) ? this.state.copiedInstance : { discoverySuppress: false, staffSuppress: false, previouslyHeld: false, source: 'FOLIO', }} visibleColumns={visibleColumns} columnMapping={columnMapping} columnWidths={{ callNumber: '15%', subject: '50%', contributor: '50%', numberOfTitles: '15%', select: '30px', title: '40%', contributorType: '15%', relatorTerm: '15%', }} getCellClass={this.formatCellStyles} customPaneSub={this.renderPaneSub()} resultsFormatter={resultsFormatter} onCreate={this.onCreate} viewRecordPerms="ui-inventory.instance.view" newRecordPerms="ui-inventory.instance.create" disableRecordCreation={disableRecordCreation || false} parentResources={parentResources} parentMutator={parentMutator} detailProps={{ referenceTables: data, onCopy: this.copyInstance, }} basePath={path} path={`${path}/(view|viewsource)/:id/:holdingsrecordid?/:itemid?`} showSingleResult={isSingleResult} renderFilters={browseFilter()} onFilterChange={this.onFilterChangeHandler} pageAmount={100} pagingType={pagingTypes.PREV_NEXT} hasNewButton={false} onResetAll={this.handleResetAll} sortableColumns={['title', 'contributors', 'publishers']} syncQueryWithUrl resultsVirtualize={false} resultsOnMarkPosition={this.onMarkPosition} resultsOnResetMarkedPosition={this.resetMarkedPosition} resultsCachedPosition={itemToView} /> </div> <ErrorModal isOpen={this.state.showErrorModal} label={<FormattedMessage id="ui-inventory.reports.inTransitItem.emptyReport.label" />} content={<FormattedMessage id="ui-inventory.reports.inTransitItem.emptyReport.message" />} onClose={this.onErrorModalClose} /> <SelectedRecordsModal isOpen={isSelectedRecordsModalOpened} records={selectedRows} columnMapping={columnMapping} formatter={resultsFormatter} onSave={this.handleSelectedRecordsModalSave} onCancel={this.handleSelectedRecordsModalCancel} /> <IfInterface name="copycat-imports"> <IfPermission perm="copycat.profiles.collection.get"> <ImportRecordModal isOpen={isImportRecordModalOpened} currentExternalIdentifier={undefined} handleSubmit={this.handleImportRecordModalSubmit} handleCancel={this.handleImportRecordModalCancel} /> </IfPermission> </IfInterface> </HasCommand> ); } } export default withNamespace(flowRight( injectIntl, withLocation, )(stripesConnect(SearchView)));
Find difference