Diff
checker
テキスト
テキスト
画像
ドキュメント
Excel
フォルダ
Legal
Enterprise
デスクトップ
料金
ログイン
Diffchecker デスクトップのダウンロード
テキスト比較
2 つのテキスト ファイルの違いを見つける
ツール
履歴
ライブエディター
未変更行を折りたたむ
折り返しなし
レイアウト
分割
統合
比較精度
スマート
単語
文字
シンタックスハイライト
構文を選択
無視
テキスト変換
最初の差分へ移動
入力を編集
Diffchecker Desktop
Diffcheckerを実行する最も安全な方法。Diffchecker Desktopアプリを入手:あなたの差分はコンピューターから出ることはありません!
Desktopを入手
Search/Browse
作成日
4 年前
差分は期限切れになりません
クリア
エクスポート
共有
説明
345 削除
行
合計
削除
文字
合計
削除
この機能を引き続き使用するには、アップグレードしてください
Diff
checker
Pro
価格を見る
700 行
すべてコピー
447 追加
行
合計
追加
文字
合計
追加
この機能を引き続き使用するには、アップグレードしてください
Diff
checker
Pro
価格を見る
807 行
すべてコピー
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,
コピー
コピー済み
コピー
コピー済み
isEmpty,
noop,
} from 'lodash';
} from 'lodash';
import {
import {
injectIntl,
injectIntl,
FormattedMessage,
FormattedMessage,
} from 'react-intl';
} from 'react-intl';
コピー
コピー済み
コピー
コピー済み
import saveAs from 'file-saver';
import moment from 'moment';
import {
import {
コピー
コピー済み
コピー
コピー済み
Pluggable,
AppIcon,
AppIcon,
コピー
コピー済み
コピー
コピー済み
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 {
コピー
コピー済み
コピー
コピー済み
Button,
Icon,
Icon,
Checkbox,
Checkbox,
コピー
コピー済み
コピー
コピー済み
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';
コピー
コピー済み
コピー
コピー済み
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';
コピー
コピー済み
コピー
コピー済み
import CheckboxColumn from '../../components/InstancesList/CheckboxColumn';
import {
import {
getCurrentFilters,
getCurrentFilters,
getNextSelectedRowsState,
getNextSelectedRowsState,
parseFiltersToStr,
parseFiltersToStr,
marshalInstance,
marshalInstance,
omitFromArray,
omitFromArray,
コピー
コピー済み
コピー
コピー済み
isTestEnv,
handleKeyCommand,
handleKeyCommand,
} from '../../utils';
} from '../../utils';
import {
import {
コピー
コピー済み
コピー
コピー済み
INSTANCES_ID_REPORT_TIMEOUT,
QUICK_EXPORT_LIMIT,
segments,
segments,
コピー
コピー済み
コピー
コピー済み
browseModeOptions,
FACETS,
} from '../../constants';
} from '../../constants';
import {
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,
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';
コピー
コピー済み
コピー
コピー済み
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';
コピー
コピー済み
コピー
コピー済み
class
InstancesList
extends React.Component {
class
SearchView
extends React.Component {
static defaultProps = {
static defaultProps = {
コピー
コピー済み
コピー
コピー済み
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,
コピー
コピー済み
コピー
コピー済み
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 = {
コピー
コピー済み
コピー
コピー済み
showNewFastAddModal: false,
inTransitItemsExportInProgress: false,
instancesIdExportInProgress: false,
holdingsIdExportInProgress: false,
instancesQuickExportInProgress: false,
showErrorModal: false,
selectedRows: {},
selectedRows: {},
コピー
コピー済み
コピー
コピー済み
optionSelected: '',
isSelectedRecordsModalOpened: false,
visibleColumns: this.getInitialToggableColumns(),
isImportRecordModalOpened: false,
searchAndSortKey: 0,
searchAndSortKey: 0,
isSingleResult: this.props.showSingleResult,
isSingleResult: this.props.showSingleResult,
};
};
}
}
コピー
コピー済み
コピー
コピー済み
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');
}
}
コピー
コピー済み
コピー
コピー済み
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.
コピー
コピー済み
コピー
コピー済み
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();
}
}
コピー
コピー済み
コピー
コピー済み
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
コピー
コピー済み
コピー
コピー済み
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} />
コピー
コピー済み
コピー
コピー済み
<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);
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);
}
}
コピー
コピー済み
コピー
コピー済み
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;
}
}
コピー
コピー済み
コピー
コピー済み
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: {},
コピー
コピー済み
コピー
コピー済み
optionSelected: '',
});
});
facetsStore.getState().resetFacetSettings();
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() {
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}`;
}
}
コピー
コピー済み
コピー
コピー済み
// 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 {
コピー
コピー済み
コピー
コピー済み
browseOnly,
disableRecordCreation,
disableRecordCreation,
intl,
intl,
data,
data,
parentResources,
parentResources,
parentMutator,
parentMutator,
renderFilters,
renderFilters,
searchableIndexes,
searchableIndexes,
match: {
match: {
path,
path,
},
},
namespace,
namespace,
stripes,
stripes,
コピー
コピー済み
コピー
コピー済み
fetchFacets,
} = this.props;
} = this.props;
const {
const {
コピー
コピー済み
コピー
コピー済み
isSelectedRecordsModalOpened,
isImportRecordModalOpened,
selectedRows,
selectedRows,
コピー
コピー済み
コピー
コピー済み
optionSelected,
searchAndSortKey,
searchAndSortKey,
isSingleResult
isSingleResult
} = this.state;
} = this.state;
const itemToView = getItem(`${namespace}.position`);
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) => {
const getFullMatchRecord = (item, isAnchor) => {
if (isAnchor) {
if (isAnchor) {
return <strong>{item}</strong>;
return <strong>{item}</strong>;
}
}
return item;
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 = {
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': ({
title,
title,
コピー
コピー済み
コピー
コピー済み
instance,
discoverySuppress,
discoverySuppress,
isBoundWith,
isBoundWith,
staffSuppress,
staffSuppress,
コピー
コピー済み
コピー
コピー済み
isAnchor,
}) => {
}) => {
コピー
コピー済み
コピー
コピー済み
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"
コピー
コピー済み
コピー
コピー済み
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"
/>
/>
コピー
コピー済み
コピー
コピー済み
}
</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);
}
}
コピー
コピー済み
コピー
コピー済み
return missedMatchItem(
);
</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 || '',
'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),
};
};
コピー
コピー済み
コピー
コピー済み
const browseQueryExecuted = Boolean(this.getExecutedBrowseQuery());
const visibleColumns = this.getVisibleColumns();
const visibleColumns = this.getVisibleColumns();
const columnMapping = this.getColumnMapping();
const columnMapping = this.getColumnMapping();
コピー
コピー済み
コピー
コピー済み
const isHandleOnNeedMore = Object.values(browseModeOptions).includes(optionSelected) ? handleOnNeedMore : null;
const onChangeIndex = (e) => {
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: '' });
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 browseFilter = () => {
コピー
コピー済み
コピー
コピー済み
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;
};
};
コピー
コピー済み
コピー
コピー済み
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 });
}
}
コピー
コピー済み
コピー
コピー済み
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)));
保存された差分
原文
ファイルを開く
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)));
変更されたテキスト
ファイルを開く
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)));
違いを見つける