Diff
checker
Text
Text
Bilder
Dokumente
Excel
Ordner
Legal
Enterprise
Desktop-App
Preise
Einloggen
Diffchecker Desktop herunterladen
Texte vergleichen
Finde den Unterschied zwischen zwei Textdateien
Werkzeuge
Verlauf
Live-Editor
Gleiches ausblenden
Zeilenumbruch aus
Ansicht
Zweispaltig
Einspaltig
Vergleichsgenauigkeit
Intelligent
Wort
Zeichen
Syntaxhervorhebung
Syntax auswählen
Ignorieren
Text umwandeln
Zur ersten Änderung
Eingabe bearbeiten
Diffchecker Desktop
Der sicherste Weg, Diffchecker zu nutzen. Hol dir die Desktop-App: Deine Diffs verlassen nie deinen Computer!
Desktop holen
Search/Browse
Erstellt
vor 4 Jahren
Diff läuft nie ab
Löschen
Exportieren
Teilen
Erklären
345 Entfernungen
Zeilen
Gesamt
Entfernt
Zeichen
Gesamt
Entfernt
Um diese Funktion weiterhin zu nutzen, aktualisiere auf
Diff
checker
Pro
Preise anzeigen
700 Zeilen
Kopieren
447 Hinzufügungen
Zeilen
Gesamt
Hinzugefügt
Zeichen
Gesamt
Hinzugefügt
Um diese Funktion weiterhin zu nutzen, aktualisiere auf
Diff
checker
Pro
Preise anzeigen
807 Zeilen
Kopieren
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,
Kopieren
Kopiert
Kopieren
Kopiert
isEmpty,
noop,
} from 'lodash';
} from 'lodash';
import {
import {
injectIntl,
injectIntl,
FormattedMessage,
FormattedMessage,
} from 'react-intl';
} from 'react-intl';
Kopieren
Kopiert
Kopieren
Kopiert
import saveAs from 'file-saver';
import moment from 'moment';
import {
import {
Kopieren
Kopiert
Kopieren
Kopiert
Pluggable,
AppIcon,
AppIcon,
Kopieren
Kopiert
Kopieren
Kopiert
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 {
Kopieren
Kopiert
Kopieren
Kopiert
Button,
Icon,
Icon,
Checkbox,
Checkbox,
Kopieren
Kopiert
Kopieren
Kopiert
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';
Kopieren
Kopiert
Kopieren
Kopiert
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';
Kopieren
Kopiert
Kopieren
Kopiert
import CheckboxColumn from '../../components/InstancesList/CheckboxColumn';
import {
import {
getCurrentFilters,
getCurrentFilters,
getNextSelectedRowsState,
getNextSelectedRowsState,
parseFiltersToStr,
parseFiltersToStr,
marshalInstance,
marshalInstance,
omitFromArray,
omitFromArray,
Kopieren
Kopiert
Kopieren
Kopiert
isTestEnv,
handleKeyCommand,
handleKeyCommand,
} from '../../utils';
} from '../../utils';
import {
import {
Kopieren
Kopiert
Kopieren
Kopiert
INSTANCES_ID_REPORT_TIMEOUT,
QUICK_EXPORT_LIMIT,
segments,
segments,
Kopieren
Kopiert
Kopieren
Kopiert
browseModeOptions,
FACETS,
} from '../../constants';
} from '../../constants';
import {
import {
Kopieren
Kopiert
Kopieren
Kopiert
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';
Kopieren
Kopiert
Kopieren
Kopiert
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';
Kopieren
Kopiert
Kopieren
Kopiert
class
InstancesList
extends React.Component {
class
SearchView
extends React.Component {
static defaultProps = {
static defaultProps = {
Kopieren
Kopiert
Kopieren
Kopiert
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,
Kopieren
Kopiert
Kopieren
Kopiert
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 = {
Kopieren
Kopiert
Kopieren
Kopiert
showNewFastAddModal: false,
inTransitItemsExportInProgress: false,
instancesIdExportInProgress: false,
holdingsIdExportInProgress: false,
instancesQuickExportInProgress: false,
showErrorModal: false,
selectedRows: {},
selectedRows: {},
Kopieren
Kopiert
Kopieren
Kopiert
optionSelected: '',
isSelectedRecordsModalOpened: false,
visibleColumns: this.getInitialToggableColumns(),
isImportRecordModalOpened: false,
searchAndSortKey: 0,
searchAndSortKey: 0,
isSingleResult: this.props.showSingleResult,
isSingleResult: this.props.showSingleResult,
};
};
}
}
Kopieren
Kopiert
Kopieren
Kopiert
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');
}
}
Kopieren
Kopiert
Kopieren
Kopiert
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.
Kopieren
Kopiert
Kopieren
Kopiert
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();
}
}
Kopieren
Kopiert
Kopieren
Kopiert
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
Kopieren
Kopiert
Kopieren
Kopiert
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} />
Kopieren
Kopiert
Kopieren
Kopiert
<FilterNavigation segment={this.props.segment} onChange={this.refocusOnInputSearch} />
</>
</>
);
);
Kopieren
Kopiert
Kopieren
Kopiert
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);
}
}
Kopieren
Kopiert
Kopieren
Kopiert
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;
}
}
Kopieren
Kopiert
Kopieren
Kopiert
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: {},
Kopieren
Kopiert
Kopieren
Kopiert
optionSelected: '',
});
});
facetsStore.getState().resetFacetSettings();
facetsStore.getState().resetFacetSettings();
}
}
Kopieren
Kopiert
Kopieren
Kopiert
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}`;
}
}
Kopieren
Kopiert
Kopieren
Kopiert
// 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 {
Kopieren
Kopiert
Kopieren
Kopiert
browseOnly,
disableRecordCreation,
disableRecordCreation,
intl,
intl,
data,
data,
parentResources,
parentResources,
parentMutator,
parentMutator,
renderFilters,
renderFilters,
searchableIndexes,
searchableIndexes,
match: {
match: {
path,
path,
},
},
namespace,
namespace,
stripes,
stripes,
Kopieren
Kopiert
Kopieren
Kopiert
fetchFacets,
} = this.props;
} = this.props;
const {
const {
Kopieren
Kopiert
Kopieren
Kopiert
isSelectedRecordsModalOpened,
isImportRecordModalOpened,
selectedRows,
selectedRows,
Kopieren
Kopiert
Kopieren
Kopiert
optionSelected,
searchAndSortKey,
searchAndSortKey,
isSingleResult
isSingleResult
} = this.state;
} = this.state;
const itemToView = getItem(`${namespace}.position`);
const itemToView = getItem(`${namespace}.position`);
Kopieren
Kopiert
Kopieren
Kopiert
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;
};
};
Kopieren
Kopiert
Kopieren
Kopiert
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 = {
Kopieren
Kopiert
Kopieren
Kopiert
'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,
Kopieren
Kopiert
Kopieren
Kopiert
instance,
discoverySuppress,
discoverySuppress,
isBoundWith,
isBoundWith,
staffSuppress,
staffSuppress,
Kopieren
Kopiert
Kopieren
Kopiert
isAnchor,
}) => {
}) => {
Kopieren
Kopiert
Kopieren
Kopiert
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"
Kopieren
Kopiert
Kopieren
Kopiert
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"
/>
/>
Kopieren
Kopiert
Kopieren
Kopiert
}
</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);
}
}
Kopieren
Kopiert
Kopieren
Kopiert
return missedMatchItem(
);
</AppIcon>
);
},
},
Kopieren
Kopiert
Kopieren
Kopiert
'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),
};
};
Kopieren
Kopiert
Kopieren
Kopiert
const browseQueryExecuted = Boolean(this.getExecutedBrowseQuery());
const visibleColumns = this.getVisibleColumns();
const visibleColumns = this.getVisibleColumns();
const columnMapping = this.getColumnMapping();
const columnMapping = this.getColumnMapping();
Kopieren
Kopiert
Kopieren
Kopiert
const isHandleOnNeedMore = Object.values(browseModeOptions).includes(optionSelected) ? handleOnNeedMore : null;
const onChangeIndex = (e) => {
const onChangeIndex = (e) => {
Kopieren
Kopiert
Kopieren
Kopiert
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: '' });
Kopieren
Kopiert
Kopieren
Kopiert
if (isBrowseOption) {
parentMutator.browseModeRecords.reset();
this.setState({ isSingleResult: false });
} else {
this.setState({ isSingleResult: true });
}
};
};
const browseFilter = () => {
const browseFilter = () => {
Kopieren
Kopiert
Kopieren
Kopiert
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;
};
};
Kopieren
Kopiert
Kopieren
Kopiert
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 });
}
}
Kopieren
Kopiert
Kopieren
Kopiert
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)));
Gespeicherte Diffs
Originaltext
Datei öffnen
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)));
Bearbeitung
Datei öffnen
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)));
Unterschied finden