Skip to content

Commit b64d759

Browse files
authored
DRYD-1938: Update Detail List View (#296)
- Split DetailItem into its own component - Create a new component for SearchResultCheckboxes - Update detail view derivative to be 'Small' - Create new config for detail view with formatters per section which match the wireframes - Create 'no image found' placeholder for BlobImage - Add alt text to BlobImage
1 parent 96e2fd4 commit b64d759

File tree

13 files changed

+559
-210
lines changed

13 files changed

+559
-210
lines changed

src/components/media/BlobImage.jsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,51 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3+
import { defineMessages, FormattedMessage } from 'react-intl';
34
import ImageContainer from '../../containers/media/ImageContainer';
45
import { getDerivativePath } from '../../helpers/blobHelpers';
56

7+
import styles from '../../../styles/cspace-ui/Image.css';
8+
69
const propTypes = {
7-
csid: PropTypes.string.isRequired,
10+
alt: PropTypes.string,
11+
csid: PropTypes.string,
812
derivative: PropTypes.string,
913
};
1014

1115
const defaultProps = {
16+
alt: '',
1217
derivative: 'Thumbnail',
1318
};
1419

20+
const messages = defineMessages({
21+
notFound: {
22+
id: 'blob.notFound',
23+
description: 'Error message when no image can be displayed',
24+
defaultMessage: 'no image found',
25+
},
26+
});
27+
28+
const renderError = () => (
29+
<div className={styles.noimage}>
30+
<FormattedMessage {...messages.notFound} />
31+
</div>
32+
);
33+
1534
export default function BlobImage(props) {
1635
const {
36+
alt,
1737
csid,
1838
derivative,
1939
} = props;
2040

41+
if (!csid) {
42+
return renderError();
43+
}
44+
2145
const path = getDerivativePath(csid, derivative);
2246

2347
return (
24-
<ImageContainer src={path} />
48+
<ImageContainer alt={alt} src={path} renderError={renderError} />
2549
);
2650
}
2751

src/components/media/Image.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ const propTypes = {
1010
alt: PropTypes.string,
1111
errorMessage: PropTypes.node,
1212
pendingMessage: PropTypes.node,
13+
renderError: PropTypes.func,
1314
retry: PropTypes.bool,
1415
retryLimit: PropTypes.number,
1516
readImage: PropTypes.func.isRequired,
1617
};
1718

19+
// todo: message -> i18n
1820
const defaultProps = {
1921
errorMessage: '',
2022
pendingMessage: '',
@@ -129,6 +131,7 @@ export default class Image extends Component {
129131
errorMessage,
130132
pendingMessage,
131133
src,
134+
renderError,
132135
retry,
133136
retryLimit,
134137
readImage,
@@ -141,9 +144,9 @@ export default class Image extends Component {
141144
} = this.state;
142145

143146
if (isError) {
144-
return (
145-
<div className={styles.error}>{errorMessage}</div>
146-
);
147+
return renderError
148+
? renderError()
149+
: <div className={styles.error}><p>{errorMessage}</p></div>;
147150
}
148151

149152
if (blobUrl) {

src/components/pages/search/SearchResults.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ export default function SearchResults(props) {
296296
);
297297

298298
return (
299-
<div className={styles.common}>
299+
<main className={styles.common}>
300300
<SearchResultTitleBar
301301
config={config}
302302
searchDescriptor={searchDescriptor}
@@ -326,7 +326,7 @@ export default function SearchResults(props) {
326326
</div>
327327
{sidebarPosition === 'right' ? sidebar : null}
328328
</div>
329-
</div>
329+
</main>
330330
);
331331
}
332332

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import { useDispatch } from 'react-redux';
3+
import PropTypes from 'prop-types';
4+
import { defineMessages, injectIntl } from 'react-intl';
5+
import { CheckboxInput } from '../../helpers/configContextInputs';
6+
import { useConfig } from '../config/ConfigProvider';
7+
import { SEARCH_RESULT_PAGE_SEARCH_NAME } from '../../constants/searchNames';
8+
import { setResultItemSelected } from '../../actions/search';
9+
10+
import styles from '../../../styles/cspace-ui/SearchTable.css';
11+
12+
const messages = defineMessages({
13+
checkboxLabelSelect: {
14+
id: 'searchResult.checkboxAriaLabel',
15+
description: 'The aria-label for a checkbox input',
16+
defaultMessage: 'Select item {index}',
17+
},
18+
});
19+
20+
function SearchResultCheckbox({
21+
index, listType, searchDescriptor, selected, intl,
22+
}) {
23+
const config = useConfig();
24+
const dispatch = useDispatch();
25+
26+
// todo: use an id field (e.g. objectNumber) instead of the index
27+
const ariaLabel = intl.formatMessage(messages.checkboxLabelSelect, { index });
28+
29+
return (
30+
<CheckboxInput
31+
embedded
32+
aria-label={ariaLabel}
33+
className={styles.detailCheckbox}
34+
name={`${index}`}
35+
value={selected}
36+
onCommit={(path, value) => dispatch(setResultItemSelected(config,
37+
SEARCH_RESULT_PAGE_SEARCH_NAME,
38+
searchDescriptor,
39+
listType,
40+
parseInt(path[0], 10),
41+
value))}
42+
onClick={(event) => event.stopPropagation()}
43+
/>
44+
);
45+
}
46+
47+
export default injectIntl(SearchResultCheckbox);
48+
49+
SearchResultCheckbox.propTypes = {
50+
index: PropTypes.number,
51+
listType: PropTypes.string,
52+
searchDescriptor: PropTypes.object,
53+
selected: PropTypes.bool,
54+
intl: PropTypes.object,
55+
};
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import React from 'react';
2+
import { Link } from 'react-router-dom';
3+
import PropTypes from 'prop-types';
4+
import Immutable from 'immutable';
5+
import {
6+
defineMessages, FormattedMessage, injectIntl, intlShape,
7+
} from 'react-intl';
8+
import { components as inputComponents } from 'cspace-input';
9+
import { useConfig } from '../../config/ConfigProvider';
10+
import BlobImage from '../../media/BlobImage';
11+
import { SEARCH_RESULT_PAGE_SEARCH_NAME } from '../../../constants/searchNames';
12+
13+
import styles from '../../../../styles/cspace-ui/SearchList.css';
14+
import SearchResultCheckbox from '../SearchResultCheckbox';
15+
16+
const { Button } = inputComponents;
17+
18+
const propTypes = {
19+
item: PropTypes.instanceOf(Immutable.Map),
20+
index: PropTypes.number,
21+
intl: intlShape,
22+
detailConfig: PropTypes.shape({
23+
aside: PropTypes.shape({
24+
formatter: PropTypes.func,
25+
}),
26+
title: PropTypes.shape({
27+
formatter: PropTypes.func,
28+
}),
29+
subtitle: PropTypes.shape({
30+
formatter: PropTypes.func,
31+
}),
32+
description: PropTypes.shape({
33+
formatter: PropTypes.func,
34+
}),
35+
tags: PropTypes.shape({
36+
formatter: PropTypes.func,
37+
}),
38+
footer: PropTypes.shape({
39+
formatter: PropTypes.func,
40+
}),
41+
}),
42+
searchDescriptor: PropTypes.object,
43+
listType: PropTypes.string,
44+
selectedItems: PropTypes.instanceOf(Immutable.Map),
45+
};
46+
47+
const renderEditButton = (location, state) => {
48+
const button = (
49+
<Button name="edit">
50+
<FormattedMessage
51+
id="searchDetailList.editLabel"
52+
description="Label of the edit record button."
53+
defaultMessage="Edit Record"
54+
/>
55+
</Button>
56+
);
57+
return location ? (
58+
<Link to={{ pathname: location, state }}>
59+
{button}
60+
</Link>
61+
) : button;
62+
};
63+
64+
const renderBlob = (location, state, blobCsid, blobAltText) => {
65+
if (location) {
66+
return (
67+
<Link to={{ pathname: location, state }}>
68+
<BlobImage csid={blobCsid} derivative="Small" alt={blobAltText} />
69+
</Link>
70+
);
71+
}
72+
73+
return <BlobImage csid={blobCsid} derivative="Small" alt={blobAltText} />;
74+
};
75+
76+
const messages = defineMessages({
77+
alt: {
78+
id: 'searchDetail.altText',
79+
description: 'Default alt text for thumbnails',
80+
defaultMessage: 'Edit record {csid}',
81+
},
82+
});
83+
84+
function DetailItem({
85+
item, index, detailConfig, searchDescriptor, listType, selectedItems, intl,
86+
}) {
87+
const config = useConfig();
88+
89+
const {
90+
title: {
91+
formatter: titleFormatter,
92+
} = {},
93+
subtitle: {
94+
formatter: subtitleFormatter,
95+
} = {},
96+
description: {
97+
formatter: descriptionFormatter,
98+
} = {},
99+
tags: {
100+
formatter: tagFormatter,
101+
} = {},
102+
footer: {
103+
formatter: footerFormatter,
104+
} = {},
105+
aside: {
106+
formatter: asideFormatter,
107+
} = {},
108+
} = detailConfig;
109+
110+
const csid = item.get('csid');
111+
const blobCsid = item.get('blobCsid');
112+
const altText = item.get('blobAltText') || intl.formatMessage(messages.alt, { csid });
113+
const selected = selectedItems ? selectedItems.has(csid) : false;
114+
115+
const listTypeConfig = config.listTypes[listType];
116+
const { getItemLocationPath } = listTypeConfig;
117+
let location;
118+
let state;
119+
if (getItemLocationPath) {
120+
location = getItemLocationPath(item, { config, searchDescriptor });
121+
state = {
122+
searchDescriptor: searchDescriptor.toJS(),
123+
// The search traverser on records will always link to the search result page, so use
124+
// its search name.
125+
searchName: SEARCH_RESULT_PAGE_SEARCH_NAME,
126+
};
127+
}
128+
129+
const renderInfo = () => <aside>{asideFormatter(item)}</aside>;
130+
const renderDescriptionBlock = () => (
131+
<div className={styles.description}>
132+
{
133+
location ? (
134+
<Link to={{ pathname: location, state }}>
135+
{titleFormatter?.(item)}
136+
</Link>
137+
) : titleFormatter?.(item)
138+
}
139+
{subtitleFormatter?.(item)}
140+
{descriptionFormatter?.(item)}
141+
{tagFormatter?.(item)}
142+
{footerFormatter?.(item)}
143+
</div>
144+
);
145+
146+
const editButton = renderEditButton(location, state);
147+
return (
148+
<div className={styles.innerDetail}>
149+
<div className={styles.imageContainer}>
150+
{renderBlob(location, state, blobCsid, altText)}
151+
</div>
152+
<SearchResultCheckbox
153+
index={index}
154+
listType={listType}
155+
searchDescriptor={searchDescriptor}
156+
selected={selected}
157+
/>
158+
{renderDescriptionBlock()}
159+
<div className={styles.info}>
160+
{renderInfo()}
161+
{editButton}
162+
</div>
163+
</div>
164+
);
165+
}
166+
167+
DetailItem.propTypes = propTypes;
168+
169+
export default injectIntl(DetailItem);

0 commit comments

Comments
 (0)