Diff
checker
文本
文本
圖像
文檔
Excel
文件夾
Legal
Enterprise
桌面版
定價
登入
下載 Diffchecker 桌面版
比較文本
尋找兩個文字檔案之間的差異
工具
歷史
即時編輯器
摺疊未變更行
關閉換行
檢視
拆分
統一
比對精度
智能
單詞
字符
語法突出顯示
選擇語法
忽略
文字轉換
前往第一個差異
編輯輸入
Diffchecker Desktop
執行Diffchecker最安全的方式。取得Diffchecker桌面應用程式:您的差異永遠不會離開您的電腦!
取得桌面版
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)));
尋找差異