Skip to content

Commit 80c577a

Browse files
demshyerezrokah
andauthored
feat(image-widget): media library gallery tools (#6087) (#6236)
Co-authored-by: Erez Rokah <[email protected]>
1 parent 1e53d35 commit 80c577a

File tree

4 files changed

+163
-43
lines changed

4 files changed

+163
-43
lines changed

packages/netlify-cms-core/src/actions/mediaLibrary.ts

+1
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ function mediaLibraryOpened(payload: {
413413
forImage?: boolean;
414414
privateUpload?: boolean;
415415
value?: string;
416+
replaceIndex?: number;
416417
allowMultiple?: boolean;
417418
config?: Map<string, unknown>;
418419
field?: EntryField;

packages/netlify-cms-core/src/reducers/mediaLibrary.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ const defaultState: {
4545
files?: MediaFile[];
4646
config: Map<string, unknown>;
4747
field?: EntryField;
48+
value?: string | string[];
49+
replaceIndex?: number;
4850
} = {
4951
isVisible: false,
5052
showMediaButton: true,
@@ -62,7 +64,8 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
6264
});
6365

6466
case MEDIA_LIBRARY_OPEN: {
65-
const { controlID, forImage, privateUpload, config, field } = action.payload;
67+
const { controlID, forImage, privateUpload, config, field, value, replaceIndex } =
68+
action.payload;
6669
const libConfig = config || Map();
6770
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
6871
if (privateUploadChanged) {
@@ -76,6 +79,8 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
7679
controlMedia: Map(),
7780
displayURLs: Map(),
7881
field,
82+
value,
83+
replaceIndex,
7984
});
8085
}
8186
return state.withMutations(map => {
@@ -86,6 +91,8 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
8691
map.set('privateUpload', privateUpload);
8792
map.set('config', libConfig);
8893
map.set('field', field);
94+
map.set('value', value);
95+
map.set('replaceIndex', replaceIndex);
8996
});
9097
}
9198

@@ -95,8 +102,25 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
95102
case MEDIA_INSERT: {
96103
const { mediaPath } = action.payload;
97104
const controlID = state.get('controlID');
105+
const value = state.get('value');
106+
107+
if (!Array.isArray(value)) {
108+
return state.withMutations(map => {
109+
map.setIn(['controlMedia', controlID], mediaPath);
110+
});
111+
}
112+
113+
const replaceIndex = state.get('replaceIndex');
114+
const mediaArray = Array.isArray(mediaPath) ? mediaPath : [mediaPath];
115+
const valueArray = value as string[];
116+
if (typeof replaceIndex == 'number') {
117+
valueArray[replaceIndex] = mediaArray[0];
118+
} else {
119+
valueArray.push(...mediaArray);
120+
}
121+
98122
return state.withMutations(map => {
99-
map.setIn(['controlMedia', controlID], mediaPath);
123+
map.setIn(['controlMedia', controlID], valueArray);
100124
});
101125
}
102126

packages/netlify-cms-locales/src/en/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -169,19 +169,25 @@ const en = {
169169
},
170170
image: {
171171
choose: 'Choose an image',
172+
chooseMultiple: 'Choose images',
172173
chooseUrl: 'Insert from URL',
173174
replaceUrl: 'Replace with URL',
174175
promptUrl: 'Enter the URL of the image',
175176
chooseDifferent: 'Choose different image',
177+
addMore: 'Add more images',
176178
remove: 'Remove image',
179+
removeAll: 'Remove all images',
177180
},
178181
file: {
179182
choose: 'Choose a file',
180183
chooseUrl: 'Insert from URL',
184+
chooseMultiple: 'Choose files',
181185
replaceUrl: 'Replace with URL',
182186
promptUrl: 'Enter the URL of the file',
183187
chooseDifferent: 'Choose different file',
188+
addMore: 'Add more files',
184189
remove: 'Remove file',
190+
removeAll: 'Remove all files',
185191
},
186192
unknownControl: {
187193
noControl: "No control for widget '%{widget}'.",

packages/netlify-cms-widget-file/src/withFileControl.js

+130-41
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ import { Map, List } from 'immutable';
77
import { once } from 'lodash';
88
import uuid from 'uuid/v4';
99
import { oneLine } from 'common-tags';
10-
import { lengths, components, buttons, borders, effects, shadows } from 'netlify-cms-ui-default';
10+
import {
11+
lengths,
12+
components,
13+
buttons,
14+
borders,
15+
effects,
16+
shadows,
17+
IconButton,
18+
} from 'netlify-cms-ui-default';
1119
import { basename } from 'netlify-cms-lib-util';
1220
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
1321
import { arrayMoveImmutable as arrayMove } from 'array-move';
@@ -28,6 +36,15 @@ const ImageWrapper = styled.div`
2836
cursor: ${props => (props.sortable ? 'pointer' : 'auto')};
2937
`;
3038

39+
const SortableImageButtonsWrapper = styled.div`
40+
display: flex;
41+
justify-content: center;
42+
column-gap: 10px;
43+
margin-right: 20px;
44+
margin-top: -10px;
45+
margin-bottom: 10px;
46+
`;
47+
3148
const StyledImage = styled.img`
3249
width: 100%;
3350
height: 100%;
@@ -38,35 +55,55 @@ function Image(props) {
3855
return <StyledImage role="presentation" {...props} />;
3956
}
4057

41-
const SortableImage = SortableElement(({ itemValue, getAsset, field }) => {
58+
function SortableImageButtons({ onRemove, onReplace }) {
4259
return (
43-
<ImageWrapper sortable>
44-
<Image src={getAsset(itemValue, field) || ''} />
45-
</ImageWrapper>
60+
<SortableImageButtonsWrapper>
61+
<IconButton size="small" type="media" onClick={onReplace}></IconButton>
62+
<IconButton size="small" type="close" onClick={onRemove}></IconButton>
63+
</SortableImageButtonsWrapper>
4664
);
47-
});
65+
}
4866

49-
const SortableMultiImageWrapper = SortableContainer(({ items, getAsset, field }) => {
67+
const SortableImage = SortableElement(({ itemValue, getAsset, field, onRemove, onReplace }) => {
5068
return (
51-
<div
52-
css={css`
53-
display: flex;
54-
flex-wrap: wrap;
55-
`}
56-
>
57-
{items.map((itemValue, index) => (
58-
<SortableImage
59-
key={`item-${itemValue}`}
60-
index={index}
61-
itemValue={itemValue}
62-
getAsset={getAsset}
63-
field={field}
64-
/>
65-
))}
69+
<div>
70+
<ImageWrapper sortable>
71+
<Image src={getAsset(itemValue, field) || ''} />
72+
</ImageWrapper>
73+
<SortableImageButtons
74+
item={itemValue}
75+
onRemove={onRemove}
76+
onReplace={onReplace}
77+
></SortableImageButtons>
6678
</div>
6779
);
6880
});
6981

82+
const SortableMultiImageWrapper = SortableContainer(
83+
({ items, getAsset, field, onRemoveOne, onReplaceOne }) => {
84+
return (
85+
<div
86+
css={css`
87+
display: flex;
88+
flex-wrap: wrap;
89+
`}
90+
>
91+
{items.map((itemValue, index) => (
92+
<SortableImage
93+
key={`item-${itemValue}`}
94+
index={index}
95+
itemValue={itemValue}
96+
getAsset={getAsset}
97+
field={field}
98+
onRemove={onRemoveOne(index)}
99+
onReplace={onReplaceOne(index)}
100+
/>
101+
))}
102+
</div>
103+
);
104+
},
105+
);
106+
70107
const FileLink = styled.a`
71108
margin-bottom: 20px;
72109
font-weight: normal;
@@ -102,6 +139,22 @@ function isMultiple(value) {
102139
return Array.isArray(value) || List.isList(value);
103140
}
104141

142+
function sizeOfValue(value) {
143+
if (Array.isArray(value)) {
144+
return value.length;
145+
}
146+
147+
if (List.isList(value)) {
148+
return value.size;
149+
}
150+
151+
return value ? 1 : 0;
152+
}
153+
154+
function valueListToArray(value) {
155+
return List.isList(value) ? value.toArray() : value;
156+
}
157+
105158
const warnDeprecatedOptions = once(field =>
106159
console.warn(oneLine`
107160
Netlify CMS config: ${field.get('name')} field: property "options" has been deprecated for the
@@ -178,26 +231,13 @@ export default function withFileControl({ forImage } = {}) {
178231
handleChange = e => {
179232
const { field, onOpenMediaLibrary, value } = this.props;
180233
e.preventDefault();
181-
let mediaLibraryFieldOptions;
182-
183-
/**
184-
* `options` hash as a general field property is deprecated, only used
185-
* when external media libraries were first introduced. Not to be
186-
* confused with `options` for the select widget, which serves a different
187-
* purpose.
188-
*/
189-
if (field.hasIn(['options', 'media_library'])) {
190-
warnDeprecatedOptions(field);
191-
mediaLibraryFieldOptions = field.getIn(['options', 'media_library'], Map());
192-
} else {
193-
mediaLibraryFieldOptions = field.get('media_library', Map());
194-
}
234+
const mediaLibraryFieldOptions = this.getMediaLibraryFieldOptions();
195235

196236
return onOpenMediaLibrary({
197237
controlID: this.controlID,
198238
forImage,
199239
privateUpload: field.get('private'),
200-
value,
240+
value: valueListToArray(value),
201241
allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true),
202242
config: mediaLibraryFieldOptions.get('config'),
203243
field,
@@ -218,6 +258,47 @@ export default function withFileControl({ forImage } = {}) {
218258
return this.props.onChange('');
219259
};
220260

261+
onRemoveOne = index => () => {
262+
const { value } = this.props;
263+
value.splice(index, 1);
264+
return this.props.onChange(sizeOfValue(value) > 0 ? [...value] : null);
265+
};
266+
267+
onReplaceOne = index => () => {
268+
const { field, onOpenMediaLibrary, value } = this.props;
269+
const mediaLibraryFieldOptions = this.getMediaLibraryFieldOptions();
270+
271+
return onOpenMediaLibrary({
272+
controlID: this.controlID,
273+
forImage,
274+
privateUpload: field.get('private'),
275+
value: valueListToArray(value),
276+
replaceIndex: index,
277+
allowMultiple: false,
278+
config: mediaLibraryFieldOptions.get('config'),
279+
field,
280+
});
281+
};
282+
283+
getMediaLibraryFieldOptions = () => {
284+
const { field } = this.props;
285+
286+
if (field.hasIn(['options', 'media_library'])) {
287+
warnDeprecatedOptions(field);
288+
return field.getIn(['options', 'media_library'], Map());
289+
}
290+
291+
return field.get('media_library', Map());
292+
};
293+
294+
allowsMultiple = () => {
295+
const mediaLibraryFieldOptions = this.getMediaLibraryFieldOptions();
296+
return (
297+
mediaLibraryFieldOptions.get('config', false) &&
298+
mediaLibraryFieldOptions.get('config').get('multiple', false)
299+
);
300+
};
301+
221302
onSortEnd = ({ oldIndex, newIndex }) => {
222303
const { value } = this.props;
223304
const newValue = arrayMove(value, oldIndex, newIndex);
@@ -274,6 +355,9 @@ export default function withFileControl({ forImage } = {}) {
274355
<SortableMultiImageWrapper
275356
items={value}
276357
onSortEnd={this.onSortEnd}
358+
onRemoveOne={this.onRemoveOne}
359+
onReplaceOne={this.onReplaceOne}
360+
distance={4}
277361
getAsset={getAsset}
278362
field={field}
279363
axis="xy"
@@ -292,21 +376,26 @@ export default function withFileControl({ forImage } = {}) {
292376

293377
renderSelection = subject => {
294378
const { t, field } = this.props;
379+
const allowsMultiple = this.allowsMultiple();
295380
return (
296381
<div>
297382
{forImage ? this.renderImages() : null}
298383
<div>
299384
{forImage ? null : this.renderFileLinks()}
300385
<FileWidgetButton onClick={this.handleChange}>
301-
{t(`editor.editorWidgets.${subject}.chooseDifferent`)}
386+
{t(
387+
`editor.editorWidgets.${subject}.${
388+
this.allowsMultiple() ? 'addMore' : 'chooseDifferent'
389+
}`,
390+
)}
302391
</FileWidgetButton>
303-
{field.get('choose_url', true) ? (
392+
{field.get('choose_url', true) && !this.allowsMultiple() ? (
304393
<FileWidgetButton onClick={this.handleUrl(subject)}>
305394
{t(`editor.editorWidgets.${subject}.replaceUrl`)}
306395
</FileWidgetButton>
307396
) : null}
308397
<FileWidgetButtonRemove onClick={this.handleRemove}>
309-
{t(`editor.editorWidgets.${subject}.remove`)}
398+
{t(`editor.editorWidgets.${subject}.remove${allowsMultiple ? 'All' : ''}`)}
310399
</FileWidgetButtonRemove>
311400
</div>
312401
</div>
@@ -318,7 +407,7 @@ export default function withFileControl({ forImage } = {}) {
318407
return (
319408
<>
320409
<FileWidgetButton onClick={this.handleChange}>
321-
{t(`editor.editorWidgets.${subject}.choose`)}
410+
{t(`editor.editorWidgets.${subject}.choose${this.allowsMultiple() ? 'Multiple' : ''}`)}
322411
</FileWidgetButton>
323412
{field.get('choose_url', true) ? (
324413
<FileWidgetButton onClick={this.handleUrl(subject)}>

0 commit comments

Comments
 (0)