decap-cms-widget-file 2.13.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "decap-cms-widget-file",
3
+ "description": "Widget for uploading files in Decap CMS.",
4
+ "version": "2.13.0-beta.0",
5
+ "homepage": "https://www.decapcms.org/docs/widgets/#file",
6
+ "repository": "https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-widget-file",
7
+ "bugs": "https://github.com/decaporg/decap-cms/issues",
8
+ "module": "dist/esm/index.js",
9
+ "main": "dist/decap-cms-widget-file.js",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "decap-cms",
13
+ "widget",
14
+ "file",
15
+ "upload",
16
+ "file-upload"
17
+ ],
18
+ "sideEffects": false,
19
+ "scripts": {
20
+ "develop": "yarn build:esm --watch",
21
+ "build": "cross-env NODE_ENV=production webpack",
22
+ "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
23
+ },
24
+ "dependencies": {
25
+ "array-move": "4.0.0",
26
+ "common-tags": "^1.8.0",
27
+ "react-sortable-hoc": "^2.0.0"
28
+ },
29
+ "peerDependencies": {
30
+ "@emotion/core": "^10.0.35",
31
+ "@emotion/styled": "^10.0.27",
32
+ "decap-cms-ui-default": "^2.12.1",
33
+ "immutable": "^3.7.6",
34
+ "prop-types": "^15.7.2",
35
+ "react": "^16.8.4 || ^17.0.0",
36
+ "react-immutable-proptypes": "^2.1.0",
37
+ "uuid": "^3.3.2"
38
+ },
39
+ "gitHead": "1bdf716e5655bf088a343cd90210a2d361a9db52"
40
+ }
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import styled from '@emotion/styled';
4
+ import { List } from 'immutable';
5
+ import { WidgetPreviewContainer } from 'decap-cms-ui-default';
6
+
7
+ const FileLink = styled(({ href, path }) => (
8
+ <a href={href} rel="noopener noreferrer" target="_blank">
9
+ {path}
10
+ </a>
11
+ ))`
12
+ display: block;
13
+ `;
14
+
15
+ function FileLinkList({ values, getAsset, field }) {
16
+ return (
17
+ <div>
18
+ {values.map(value => (
19
+ <FileLink key={value} path={value} href={getAsset(value, field)} />
20
+ ))}
21
+ </div>
22
+ );
23
+ }
24
+
25
+ function FileContent(props) {
26
+ const { value, getAsset, field } = props;
27
+ if (Array.isArray(value) || List.isList(value)) {
28
+ return <FileLinkList values={value} getAsset={getAsset} field={field} />;
29
+ }
30
+ return <FileLink key={value} path={value} href={getAsset(value, field)} />;
31
+ }
32
+
33
+ function FilePreview(props) {
34
+ return (
35
+ <WidgetPreviewContainer>
36
+ {props.value ? <FileContent {...props} /> : null}
37
+ </WidgetPreviewContainer>
38
+ );
39
+ }
40
+
41
+ FilePreview.propTypes = {
42
+ getAsset: PropTypes.func.isRequired,
43
+ value: PropTypes.node,
44
+ };
45
+
46
+ export default FilePreview;
package/src/index.js ADDED
@@ -0,0 +1,18 @@
1
+ import withFileControl from './withFileControl';
2
+ import previewComponent from './FilePreview';
3
+ import schema from './schema';
4
+
5
+ const controlComponent = withFileControl();
6
+
7
+ function Widget(opts = {}) {
8
+ return {
9
+ name: 'file',
10
+ controlComponent,
11
+ previewComponent,
12
+ schema,
13
+ ...opts,
14
+ };
15
+ }
16
+
17
+ export const DecapCmsWidgetFile = { Widget, controlComponent, previewComponent, withFileControl };
18
+ export default DecapCmsWidgetFile;
package/src/schema.js ADDED
@@ -0,0 +1,5 @@
1
+ export default {
2
+ properties: {
3
+ allow_multiple: { type: 'boolean' },
4
+ },
5
+ };
@@ -0,0 +1,429 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import ImmutablePropTypes from 'react-immutable-proptypes';
4
+ import styled from '@emotion/styled';
5
+ import { css } from '@emotion/core';
6
+ import { Map, List } from 'immutable';
7
+ import { once } from 'lodash';
8
+ import uuid from 'uuid/v4';
9
+ import { oneLine } from 'common-tags';
10
+ import {
11
+ lengths,
12
+ components,
13
+ buttons,
14
+ borders,
15
+ effects,
16
+ shadows,
17
+ IconButton,
18
+ } from 'decap-cms-ui-default';
19
+ import { basename } from 'decap-cms-lib-util';
20
+ import { SortableContainer, SortableElement } from 'react-sortable-hoc';
21
+ import { arrayMoveImmutable as arrayMove } from 'array-move';
22
+
23
+ const MAX_DISPLAY_LENGTH = 50;
24
+
25
+ const ImageWrapper = styled.div`
26
+ flex-basis: 155px;
27
+ width: 155px;
28
+ height: 100px;
29
+ margin-right: 20px;
30
+ margin-bottom: 20px;
31
+ border: ${borders.textField};
32
+ border-radius: ${lengths.borderRadius};
33
+ overflow: hidden;
34
+ ${effects.checkerboard};
35
+ ${shadows.inset};
36
+ cursor: ${props => (props.sortable ? 'pointer' : 'auto')};
37
+ `;
38
+
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
+
48
+ const StyledImage = styled.img`
49
+ width: 100%;
50
+ height: 100%;
51
+ object-fit: contain;
52
+ `;
53
+
54
+ function Image(props) {
55
+ return <StyledImage role="presentation" {...props} />;
56
+ }
57
+
58
+ function SortableImageButtons({ onRemove, onReplace }) {
59
+ return (
60
+ <SortableImageButtonsWrapper>
61
+ <IconButton size="small" type="media" onClick={onReplace}></IconButton>
62
+ <IconButton size="small" type="close" onClick={onRemove}></IconButton>
63
+ </SortableImageButtonsWrapper>
64
+ );
65
+ }
66
+
67
+ const SortableImage = SortableElement(({ itemValue, getAsset, field, onRemove, onReplace }) => {
68
+ return (
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>
78
+ </div>
79
+ );
80
+ });
81
+
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
+
107
+ const FileLink = styled.a`
108
+ margin-bottom: 20px;
109
+ font-weight: normal;
110
+ color: inherit;
111
+
112
+ &:hover,
113
+ &:active,
114
+ &:focus {
115
+ text-decoration: underline;
116
+ }
117
+ `;
118
+
119
+ const FileLinks = styled.div`
120
+ margin-bottom: 12px;
121
+ `;
122
+
123
+ const FileLinkList = styled.ul`
124
+ list-style-type: none;
125
+ `;
126
+
127
+ const FileWidgetButton = styled.button`
128
+ ${buttons.button};
129
+ ${components.badge};
130
+ margin-bottom: 12px;
131
+ `;
132
+
133
+ const FileWidgetButtonRemove = styled.button`
134
+ ${buttons.button};
135
+ ${components.badgeDanger};
136
+ `;
137
+
138
+ function isMultiple(value) {
139
+ return Array.isArray(value) || List.isList(value);
140
+ }
141
+
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
+
158
+ const warnDeprecatedOptions = once(field =>
159
+ console.warn(oneLine`
160
+ Decap CMS config: ${field.get('name')} field: property "options" has been deprecated for the
161
+ ${field.get('widget')} widget and will be removed in the next major release. Rather than
162
+ \`field.options.media_library\`, apply media library options for this widget under
163
+ \`field.media_library\`.
164
+ `),
165
+ );
166
+
167
+ export default function withFileControl({ forImage } = {}) {
168
+ return class FileControl extends React.Component {
169
+ static propTypes = {
170
+ field: PropTypes.object.isRequired,
171
+ getAsset: PropTypes.func.isRequired,
172
+ mediaPaths: ImmutablePropTypes.map.isRequired,
173
+ onAddAsset: PropTypes.func.isRequired,
174
+ onChange: PropTypes.func.isRequired,
175
+ onRemoveInsertedMedia: PropTypes.func.isRequired,
176
+ onOpenMediaLibrary: PropTypes.func.isRequired,
177
+ onClearMediaControl: PropTypes.func.isRequired,
178
+ onRemoveMediaControl: PropTypes.func.isRequired,
179
+ classNameWrapper: PropTypes.string.isRequired,
180
+ value: PropTypes.oneOfType([
181
+ PropTypes.string,
182
+ PropTypes.arrayOf(PropTypes.string),
183
+ ImmutablePropTypes.listOf(PropTypes.string),
184
+ ]),
185
+ t: PropTypes.func.isRequired,
186
+ };
187
+
188
+ static defaultProps = {
189
+ value: '',
190
+ };
191
+
192
+ constructor(props) {
193
+ super(props);
194
+ this.controlID = uuid();
195
+ }
196
+
197
+ shouldComponentUpdate(nextProps) {
198
+ /**
199
+ * Always update if the value or getAsset changes.
200
+ */
201
+ if (this.props.value !== nextProps.value || this.props.getAsset !== nextProps.getAsset) {
202
+ return true;
203
+ }
204
+
205
+ /**
206
+ * If there is a media path for this control in the state object, and that
207
+ * path is different than the value in `nextProps`, update.
208
+ */
209
+ const mediaPath = nextProps.mediaPaths.get(this.controlID);
210
+ if (mediaPath && nextProps.value !== mediaPath) {
211
+ return true;
212
+ }
213
+
214
+ return false;
215
+ }
216
+
217
+ componentDidUpdate() {
218
+ const { mediaPaths, value, onRemoveInsertedMedia, onChange } = this.props;
219
+ const mediaPath = mediaPaths.get(this.controlID);
220
+ if (mediaPath && mediaPath !== value) {
221
+ onChange(mediaPath);
222
+ } else if (mediaPath && mediaPath === value) {
223
+ onRemoveInsertedMedia(this.controlID);
224
+ }
225
+ }
226
+
227
+ componentWillUnmount() {
228
+ this.props.onRemoveMediaControl(this.controlID);
229
+ }
230
+
231
+ handleChange = e => {
232
+ const { field, onOpenMediaLibrary, value } = this.props;
233
+ e.preventDefault();
234
+ const mediaLibraryFieldOptions = this.getMediaLibraryFieldOptions();
235
+
236
+ return onOpenMediaLibrary({
237
+ controlID: this.controlID,
238
+ forImage,
239
+ privateUpload: field.get('private'),
240
+ value: valueListToArray(value),
241
+ allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true),
242
+ config: mediaLibraryFieldOptions.get('config'),
243
+ field,
244
+ });
245
+ };
246
+
247
+ handleUrl = subject => e => {
248
+ e.preventDefault();
249
+
250
+ const url = window.prompt(this.props.t(`editor.editorWidgets.${subject}.promptUrl`));
251
+
252
+ return this.props.onChange(url);
253
+ };
254
+
255
+ handleRemove = e => {
256
+ e.preventDefault();
257
+ this.props.onClearMediaControl(this.controlID);
258
+ return this.props.onChange('');
259
+ };
260
+
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
+
302
+ onSortEnd = ({ oldIndex, newIndex }) => {
303
+ const { value } = this.props;
304
+ const newValue = arrayMove(value, oldIndex, newIndex);
305
+ return this.props.onChange(newValue);
306
+ };
307
+
308
+ getValidateValue = () => {
309
+ const { value } = this.props;
310
+ if (value) {
311
+ return isMultiple(value) ? value.map(v => basename(v)) : basename(value);
312
+ }
313
+
314
+ return value;
315
+ };
316
+
317
+ renderFileLink = value => {
318
+ const size = MAX_DISPLAY_LENGTH;
319
+ if (!value || value.length <= size) {
320
+ return value;
321
+ }
322
+ const text = `${value.slice(0, size / 2)}\u2026${value.slice(-(size / 2) + 1)}`;
323
+ return (
324
+ <FileLink href={value} rel="noopener" target="_blank">
325
+ {text}
326
+ </FileLink>
327
+ );
328
+ };
329
+
330
+ renderFileLinks = () => {
331
+ const { value } = this.props;
332
+
333
+ if (isMultiple(value)) {
334
+ return (
335
+ <FileLinks>
336
+ <FileLinkList>
337
+ {value.map(val => (
338
+ <li key={val}>{this.renderFileLink(val)}</li>
339
+ ))}
340
+ </FileLinkList>
341
+ </FileLinks>
342
+ );
343
+ }
344
+ return <FileLinks>{this.renderFileLink(value)}</FileLinks>;
345
+ };
346
+
347
+ renderImages = () => {
348
+ const { getAsset, value, field } = this.props;
349
+
350
+ if (isMultiple(value)) {
351
+ return (
352
+ <SortableMultiImageWrapper
353
+ items={value}
354
+ onSortEnd={this.onSortEnd}
355
+ onRemoveOne={this.onRemoveOne}
356
+ onReplaceOne={this.onReplaceOne}
357
+ distance={4}
358
+ getAsset={getAsset}
359
+ field={field}
360
+ axis="xy"
361
+ lockToContainerEdges={true}
362
+ ></SortableMultiImageWrapper>
363
+ );
364
+ }
365
+
366
+ const src = getAsset(value, field);
367
+ return (
368
+ <ImageWrapper>
369
+ <Image src={src || ''} />
370
+ </ImageWrapper>
371
+ );
372
+ };
373
+
374
+ renderSelection = subject => {
375
+ const { t, field } = this.props;
376
+ const allowsMultiple = this.allowsMultiple();
377
+ return (
378
+ <div>
379
+ {forImage ? this.renderImages() : null}
380
+ <div>
381
+ {forImage ? null : this.renderFileLinks()}
382
+ <FileWidgetButton onClick={this.handleChange}>
383
+ {t(
384
+ `editor.editorWidgets.${subject}.${
385
+ this.allowsMultiple() ? 'addMore' : 'chooseDifferent'
386
+ }`,
387
+ )}
388
+ </FileWidgetButton>
389
+ {field.get('choose_url', true) && !this.allowsMultiple() ? (
390
+ <FileWidgetButton onClick={this.handleUrl(subject)}>
391
+ {t(`editor.editorWidgets.${subject}.replaceUrl`)}
392
+ </FileWidgetButton>
393
+ ) : null}
394
+ <FileWidgetButtonRemove onClick={this.handleRemove}>
395
+ {t(`editor.editorWidgets.${subject}.remove${allowsMultiple ? 'All' : ''}`)}
396
+ </FileWidgetButtonRemove>
397
+ </div>
398
+ </div>
399
+ );
400
+ };
401
+
402
+ renderNoSelection = subject => {
403
+ const { t, field } = this.props;
404
+ return (
405
+ <>
406
+ <FileWidgetButton onClick={this.handleChange}>
407
+ {t(`editor.editorWidgets.${subject}.choose${this.allowsMultiple() ? 'Multiple' : ''}`)}
408
+ </FileWidgetButton>
409
+ {field.get('choose_url', true) ? (
410
+ <FileWidgetButton onClick={this.handleUrl(subject)}>
411
+ {t(`editor.editorWidgets.${subject}.chooseUrl`)}
412
+ </FileWidgetButton>
413
+ ) : null}
414
+ </>
415
+ );
416
+ };
417
+
418
+ render() {
419
+ const { value, classNameWrapper } = this.props;
420
+ const subject = forImage ? 'image' : 'file';
421
+
422
+ return (
423
+ <div className={classNameWrapper}>
424
+ <span>{value ? this.renderSelection(subject) : this.renderNoSelection(subject)}</span>
425
+ </div>
426
+ );
427
+ }
428
+ };
429
+ }
@@ -0,0 +1,3 @@
1
+ const { getConfig } = require('../../scripts/webpack.js');
2
+
3
+ module.exports = getConfig();