Skip to content

Commit

Permalink
feat(image-widget): media library gallery tools (#6087)
Browse files Browse the repository at this point in the history
  • Loading branch information
demshy authored and erezrokah committed Mar 14, 2022
1 parent 964d697 commit 8e3e16c
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 43 deletions.
1 change: 1 addition & 0 deletions packages/netlify-cms-core/src/actions/mediaLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ function mediaLibraryOpened(payload: {
forImage?: boolean;
privateUpload?: boolean;
value?: string;
replaceIndex?: number;
allowMultiple?: boolean;
config?: Map<string, unknown>;
field?: EntryField;
Expand Down
28 changes: 26 additions & 2 deletions packages/netlify-cms-core/src/reducers/mediaLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const defaultState: {
files?: MediaFile[];
config: Map<string, unknown>;
field?: EntryField;
value?: string | string[];
replaceIndex?: number;
} = {
isVisible: false,
showMediaButton: true,
Expand All @@ -62,7 +64,8 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
});

case MEDIA_LIBRARY_OPEN: {
const { controlID, forImage, privateUpload, config, field } = action.payload;
const { controlID, forImage, privateUpload, config, field, value, replaceIndex } =
action.payload;
const libConfig = config || Map();
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
if (privateUploadChanged) {
Expand All @@ -76,6 +79,8 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
controlMedia: Map(),
displayURLs: Map(),
field,
value,
replaceIndex,
});
}
return state.withMutations(map => {
Expand All @@ -86,6 +91,8 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
map.set('privateUpload', privateUpload);
map.set('config', libConfig);
map.set('field', field);
map.set('value', value);
map.set('replaceIndex', replaceIndex);
});
}

Expand All @@ -95,8 +102,25 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
case MEDIA_INSERT: {
const { mediaPath } = action.payload;
const controlID = state.get('controlID');
const value = state.get('value');

if (!Array.isArray(value)) {
return state.withMutations(map => {
map.setIn(['controlMedia', controlID], mediaPath);
});
}

const replaceIndex = state.get('replaceIndex');
const mediaArray = Array.isArray(mediaPath) ? mediaPath : [mediaPath];
const valueArray = value as string[];
if (typeof replaceIndex == 'number') {
valueArray[replaceIndex] = mediaArray[0];
} else {
valueArray.push(...mediaArray);
}

return state.withMutations(map => {
map.setIn(['controlMedia', controlID], mediaPath);
map.setIn(['controlMedia', controlID], valueArray);
});
}

Expand Down
6 changes: 6 additions & 0 deletions packages/netlify-cms-locales/src/en/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,19 +169,25 @@ const en = {
},
image: {
choose: 'Choose an image',
chooseMultiple: 'Choose images',
chooseUrl: 'Insert from URL',
replaceUrl: 'Replace with URL',
promptUrl: 'Enter the URL of the image',
chooseDifferent: 'Choose different image',
addMore: 'Add more images',
remove: 'Remove image',
removeAll: 'Remove all images',
},
file: {
choose: 'Choose a file',
chooseUrl: 'Insert from URL',
chooseMultiple: 'Choose files',
replaceUrl: 'Replace with URL',
promptUrl: 'Enter the URL of the file',
chooseDifferent: 'Choose different file',
addMore: 'Add more files',
remove: 'Remove file',
removeAll: 'Remove all files',
},
unknownControl: {
noControl: "No control for widget '%{widget}'.",
Expand Down
171 changes: 130 additions & 41 deletions packages/netlify-cms-widget-file/src/withFileControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ import { Map, List } from 'immutable';
import { once } from 'lodash';
import uuid from 'uuid/v4';
import { oneLine } from 'common-tags';
import { lengths, components, buttons, borders, effects, shadows } from 'netlify-cms-ui-default';
import {
lengths,
components,
buttons,
borders,
effects,
shadows,
IconButton,
} from 'netlify-cms-ui-default';
import { basename } from 'netlify-cms-lib-util';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
import { arrayMoveImmutable as arrayMove } from 'array-move';
Expand All @@ -28,6 +36,15 @@ const ImageWrapper = styled.div`
cursor: ${props => (props.sortable ? 'pointer' : 'auto')};
`;

const SortableImageButtonsWrapper = styled.div`
display: flex;
justify-content: center;
column-gap: 10px;
margin-right: 20px;
margin-top: -10px;
margin-bottom: 10px;
`;

const StyledImage = styled.img`
width: 100%;
height: 100%;
Expand All @@ -38,35 +55,55 @@ function Image(props) {
return <StyledImage role="presentation" {...props} />;
}

const SortableImage = SortableElement(({ itemValue, getAsset, field }) => {
function SortableImageButtons({ onRemove, onReplace }) {
return (
<ImageWrapper sortable>
<Image src={getAsset(itemValue, field) || ''} />
</ImageWrapper>
<SortableImageButtonsWrapper>
<IconButton size="small" type="media" onClick={onReplace}></IconButton>
<IconButton size="small" type="close" onClick={onRemove}></IconButton>
</SortableImageButtonsWrapper>
);
});
}

const SortableMultiImageWrapper = SortableContainer(({ items, getAsset, field }) => {
const SortableImage = SortableElement(({ itemValue, getAsset, field, onRemove, onReplace }) => {
return (
<div
css={css`
display: flex;
flex-wrap: wrap;
`}
>
{items.map((itemValue, index) => (
<SortableImage
key={`item-${itemValue}`}
index={index}
itemValue={itemValue}
getAsset={getAsset}
field={field}
/>
))}
<div>
<ImageWrapper sortable>
<Image src={getAsset(itemValue, field) || ''} />
</ImageWrapper>
<SortableImageButtons
item={itemValue}
onRemove={onRemove}
onReplace={onReplace}
></SortableImageButtons>
</div>
);
});

const SortableMultiImageWrapper = SortableContainer(
({ items, getAsset, field, onRemoveOne, onReplaceOne }) => {
return (
<div
css={css`
display: flex;
flex-wrap: wrap;
`}
>
{items.map((itemValue, index) => (
<SortableImage
key={`item-${itemValue}`}
index={index}
itemValue={itemValue}
getAsset={getAsset}
field={field}
onRemove={onRemoveOne(index)}
onReplace={onReplaceOne(index)}
/>
))}
</div>
);
},
);

const FileLink = styled.a`
margin-bottom: 20px;
font-weight: normal;
Expand Down Expand Up @@ -102,6 +139,22 @@ function isMultiple(value) {
return Array.isArray(value) || List.isList(value);
}

function sizeOfValue(value) {
if (Array.isArray(value)) {
return value.length;
}

if (List.isList(value)) {
return value.size;
}

return value ? 1 : 0;
}

function valueListToArray(value) {
return List.isList(value) ? value.toArray() : value;
}

const warnDeprecatedOptions = once(field =>
console.warn(oneLine`
Netlify CMS config: ${field.get('name')} field: property "options" has been deprecated for the
Expand Down Expand Up @@ -178,26 +231,13 @@ export default function withFileControl({ forImage } = {}) {
handleChange = e => {
const { field, onOpenMediaLibrary, value } = this.props;
e.preventDefault();
let mediaLibraryFieldOptions;

/**
* `options` hash as a general field property is deprecated, only used
* when external media libraries were first introduced. Not to be
* confused with `options` for the select widget, which serves a different
* purpose.
*/
if (field.hasIn(['options', 'media_library'])) {
warnDeprecatedOptions(field);
mediaLibraryFieldOptions = field.getIn(['options', 'media_library'], Map());
} else {
mediaLibraryFieldOptions = field.get('media_library', Map());
}
const mediaLibraryFieldOptions = this.getMediaLibraryFieldOptions();

return onOpenMediaLibrary({
controlID: this.controlID,
forImage,
privateUpload: field.get('private'),
value,
value: valueListToArray(value),
allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true),
config: mediaLibraryFieldOptions.get('config'),
field,
Expand All @@ -218,6 +258,47 @@ export default function withFileControl({ forImage } = {}) {
return this.props.onChange('');
};

onRemoveOne = index => () => {
const { value } = this.props;
value.splice(index, 1);
return this.props.onChange(sizeOfValue(value) > 0 ? [...value] : null);
};

onReplaceOne = index => () => {
const { field, onOpenMediaLibrary, value } = this.props;
const mediaLibraryFieldOptions = this.getMediaLibraryFieldOptions();

return onOpenMediaLibrary({
controlID: this.controlID,
forImage,
privateUpload: field.get('private'),
value: valueListToArray(value),
replaceIndex: index,
allowMultiple: false,
config: mediaLibraryFieldOptions.get('config'),
field,
});
};

getMediaLibraryFieldOptions = () => {
const { field } = this.props;

if (field.hasIn(['options', 'media_library'])) {
warnDeprecatedOptions(field);
return field.getIn(['options', 'media_library'], Map());
}

return field.get('media_library', Map());
};

allowsMultiple = () => {
const mediaLibraryFieldOptions = this.getMediaLibraryFieldOptions();
return (
mediaLibraryFieldOptions.get('config', false) &&
mediaLibraryFieldOptions.get('config').get('multiple', false)
);
};

onSortEnd = ({ oldIndex, newIndex }) => {
const { value } = this.props;
const newValue = arrayMove(value, oldIndex, newIndex);
Expand Down Expand Up @@ -274,6 +355,9 @@ export default function withFileControl({ forImage } = {}) {
<SortableMultiImageWrapper
items={value}
onSortEnd={this.onSortEnd}
onRemoveOne={this.onRemoveOne}
onReplaceOne={this.onReplaceOne}
distance={4}
getAsset={getAsset}
field={field}
axis="xy"
Expand All @@ -292,21 +376,26 @@ export default function withFileControl({ forImage } = {}) {

renderSelection = subject => {
const { t, field } = this.props;
const allowsMultiple = this.allowsMultiple();
return (
<div>
{forImage ? this.renderImages() : null}
<div>
{forImage ? null : this.renderFileLinks()}
<FileWidgetButton onClick={this.handleChange}>
{t(`editor.editorWidgets.${subject}.chooseDifferent`)}
{t(
`editor.editorWidgets.${subject}.${
this.allowsMultiple() ? 'addMore' : 'chooseDifferent'
}`,
)}
</FileWidgetButton>
{field.get('choose_url', true) ? (
{field.get('choose_url', true) && !this.allowsMultiple() ? (
<FileWidgetButton onClick={this.handleUrl(subject)}>
{t(`editor.editorWidgets.${subject}.replaceUrl`)}
</FileWidgetButton>
) : null}
<FileWidgetButtonRemove onClick={this.handleRemove}>
{t(`editor.editorWidgets.${subject}.remove`)}
{t(`editor.editorWidgets.${subject}.remove${allowsMultiple ? 'All' : ''}`)}
</FileWidgetButtonRemove>
</div>
</div>
Expand All @@ -318,7 +407,7 @@ export default function withFileControl({ forImage } = {}) {
return (
<>
<FileWidgetButton onClick={this.handleChange}>
{t(`editor.editorWidgets.${subject}.choose`)}
{t(`editor.editorWidgets.${subject}.choose${this.allowsMultiple() ? 'Multiple' : ''}`)}
</FileWidgetButton>
{field.get('choose_url', true) ? (
<FileWidgetButton onClick={this.handleUrl(subject)}>
Expand Down

0 comments on commit 8e3e16c

Please sign in to comment.