@plone/volto 18.10.1 → 18.11.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/CHANGELOG.md +53 -0
- package/README.md +3 -3
- package/locales/ca/LC_MESSAGES/volto.po +46 -0
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +46 -0
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +46 -0
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +46 -0
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +46 -0
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +46 -0
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +46 -0
- package/locales/fr.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +46 -0
- package/locales/hi.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +46 -0
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +46 -0
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +46 -0
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +46 -0
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +47 -1
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +46 -0
- package/locales/ro.json +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +5243 -0
- package/locales/ru.json +1 -0
- package/locales/volto.pot +47 -1
- package/locales/zh_CN/LC_MESSAGES/volto.po +46 -0
- package/locales/zh_CN.json +1 -1
- package/package.json +4 -4
- package/razzle.config.js +1 -1
- package/src/components/index.js +2 -0
- package/src/components/manage/Blocks/Block/BlocksForm.jsx +5 -1
- package/src/components/manage/Blocks/Search/components/SearchInput.jsx +11 -1
- package/src/components/manage/DragDropList/DragDropList.jsx +78 -33
- package/src/components/manage/Form/Field.jsx +38 -29
- package/src/components/manage/Form/Form.jsx +141 -69
- package/src/components/manage/Form/ModalForm.jsx +29 -0
- package/src/components/manage/Sidebar/ObjectBrowserNav.jsx +1 -1
- package/src/components/manage/Sidebar/ObjectBrowserNav.test.jsx +34 -13
- package/src/components/manage/Widgets/CheckboxGroupWidget.jsx +214 -0
- package/src/components/manage/Widgets/CheckboxGroupWidget.stories.jsx +39 -0
- package/src/components/manage/Widgets/CheckboxGroupWidget.test.jsx +45 -0
- package/src/components/manage/Widgets/FileWidget.jsx +43 -2
- package/src/components/manage/Widgets/HiddenWidget.jsx +114 -0
- package/src/components/manage/Widgets/HiddenWidget.stories.jsx +20 -0
- package/src/components/manage/Widgets/HiddenWidget.test.jsx +32 -0
- package/src/components/manage/Widgets/ImageWidget.jsx +3 -0
- package/src/components/manage/Widgets/RadioGroupWidget.jsx +207 -0
- package/src/components/manage/Widgets/RadioGroupWidget.stories.jsx +39 -0
- package/src/components/manage/Widgets/RadioGroupWidget.test.jsx +46 -0
- package/src/components/manage/Widgets/SchemaWidget.jsx +806 -320
- package/src/components/manage/Widgets/SelectWidget.jsx +38 -2
- package/src/components/manage/Widgets/StaticTextWidget.jsx +34 -0
- package/src/components/manage/Widgets/StaticTextWidget.stories.jsx +20 -0
- package/src/components/manage/Widgets/StaticTextWidget.test.jsx +25 -0
- package/src/components/manage/Widgets/TimeWidget.jsx +96 -0
- package/src/components/manage/Widgets/TimeWidget.stories.jsx +22 -0
- package/src/components/manage/Widgets/TimeWidget.test.jsx +35 -0
- package/src/components/manage/Widgets/index.tsx +35 -0
- package/src/components/theme/ContactForm/ContactForm.jsx +7 -4
- package/src/components/theme/Widgets/HiddenWidget.jsx +2 -0
- package/src/components/theme/Widgets/HiddenWidget.stories.jsx +25 -0
- package/src/components/theme/Widgets/HiddenWidget.test.jsx +11 -0
- package/src/components/theme/Widgets/StaticTextWidget.jsx +16 -0
- package/src/components/theme/Widgets/StaticTextWidget.stories.jsx +29 -0
- package/src/components/theme/Widgets/StaticTextWidget.test.jsx +32 -0
- package/src/config/Blocks.jsx +2 -0
- package/src/config/Widgets.jsx +14 -0
- package/src/constants/Languages.cjs +1 -0
- package/src/helpers/FormValidation/FormValidation.jsx +4 -0
- package/src/helpers/FormValidation/FormValidation.test.js +147 -31
- package/src/helpers/FormValidation/validators.ts +4 -1
- package/src/helpers/Utils/Utils.jsx +14 -2
- package/theme/themes/pastanaga/collections/form.overrides +4 -0
- package/theme/themes/pastanaga/elements/input.overrides +7 -0
- package/theme/themes/pastanaga/extras/sidebar.less +2 -0
- package/types/components/index.d.ts +1 -1
- package/types/components/manage/Widgets/CheckboxGroupWidget.d.ts +6 -0
- package/types/components/manage/Widgets/CheckboxGroupWidget.stories.d.ts +15 -0
- package/types/components/manage/Widgets/CheckboxGroupWidget.test.d.ts +1 -0
- package/types/components/manage/Widgets/HiddenWidget.d.ts +54 -0
- package/types/components/manage/Widgets/HiddenWidget.stories.d.ts +9 -0
- package/types/components/manage/Widgets/HiddenWidget.test.d.ts +1 -0
- package/types/components/manage/Widgets/RadioGroupWidget.d.ts +6 -0
- package/types/components/manage/Widgets/RadioGroupWidget.stories.d.ts +15 -0
- package/types/components/manage/Widgets/RadioGroupWidget.test.d.ts +1 -0
- package/types/components/manage/Widgets/StaticTextWidget.d.ts +18 -0
- package/types/components/manage/Widgets/StaticTextWidget.stories.d.ts +9 -0
- package/types/components/manage/Widgets/StaticTextWidget.test.d.ts +1 -0
- package/types/components/manage/Widgets/TimeWidget.d.ts +2 -0
- package/types/components/manage/Widgets/TimeWidget.stories.d.ts +8 -0
- package/types/components/manage/Widgets/TimeWidget.test.d.ts +1 -0
- package/types/components/manage/Widgets/index.d.ts +5 -0
- package/types/components/theme/Widgets/HiddenWidget.d.ts +6 -0
- package/types/components/theme/Widgets/HiddenWidget.stories.d.ts +8 -0
- package/types/components/theme/Widgets/HiddenWidget.test.d.ts +1 -0
- package/types/components/theme/Widgets/StaticTextWidget.d.ts +5 -0
- package/types/components/theme/Widgets/StaticTextWidget.stories.d.ts +8 -0
- package/types/components/theme/Widgets/StaticTextWidget.test.d.ts +1 -0
- package/types/config/Widgets.d.ts +14 -0
- package/types/constants/Languages.d.cts +1 -0
- package/types/helpers/FormValidation/validators.d.ts +1 -1
- package/types/helpers/Utils/Utils.d.ts +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CheckboxGroupWidget component.
|
|
3
|
+
* @module components/manage/Widgets/CheckboxGroupWidget
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { Component } from 'react';
|
|
7
|
+
import PropTypes from 'prop-types';
|
|
8
|
+
import { connect } from 'react-redux';
|
|
9
|
+
import filter from 'lodash/filter';
|
|
10
|
+
import includes from 'lodash/includes';
|
|
11
|
+
import map from 'lodash/map';
|
|
12
|
+
import without from 'lodash/without';
|
|
13
|
+
import { injectIntl } from 'react-intl';
|
|
14
|
+
import {
|
|
15
|
+
getVocabFromHint,
|
|
16
|
+
getVocabFromField,
|
|
17
|
+
getVocabFromItems,
|
|
18
|
+
} from '@plone/volto/helpers/Vocabularies/Vocabularies';
|
|
19
|
+
import { Checkbox } from 'semantic-ui-react';
|
|
20
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
21
|
+
import {
|
|
22
|
+
getVocabulary,
|
|
23
|
+
getVocabularyTokenTitle,
|
|
24
|
+
} from '@plone/volto/actions/vocabularies/vocabularies';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* CheckboxGroupWidget component class.
|
|
28
|
+
* @function CheckboxGroupWidget
|
|
29
|
+
* @returns {string} Markup of the component.
|
|
30
|
+
*/
|
|
31
|
+
class CheckboxGroupWidget extends Component {
|
|
32
|
+
/**
|
|
33
|
+
* Property types.
|
|
34
|
+
* @property {Object} propTypes Property types.
|
|
35
|
+
* @static
|
|
36
|
+
*/
|
|
37
|
+
static propTypes = {
|
|
38
|
+
id: PropTypes.string.isRequired,
|
|
39
|
+
title: PropTypes.string.isRequired,
|
|
40
|
+
description: PropTypes.string,
|
|
41
|
+
required: PropTypes.bool,
|
|
42
|
+
filterChoices: PropTypes.arrayOf(PropTypes.string),
|
|
43
|
+
error: PropTypes.arrayOf(PropTypes.string),
|
|
44
|
+
getVocabulary: PropTypes.func.isRequired,
|
|
45
|
+
getVocabularyTokenTitle: PropTypes.func.isRequired,
|
|
46
|
+
choices: PropTypes.arrayOf(
|
|
47
|
+
PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
|
48
|
+
),
|
|
49
|
+
items: PropTypes.shape({
|
|
50
|
+
vocabulary: PropTypes.object,
|
|
51
|
+
}),
|
|
52
|
+
widgetOptions: PropTypes.shape({
|
|
53
|
+
vocabulary: PropTypes.object,
|
|
54
|
+
}),
|
|
55
|
+
value: PropTypes.oneOfType([
|
|
56
|
+
PropTypes.object,
|
|
57
|
+
PropTypes.string,
|
|
58
|
+
PropTypes.bool,
|
|
59
|
+
PropTypes.func,
|
|
60
|
+
PropTypes.array,
|
|
61
|
+
]),
|
|
62
|
+
onChange: PropTypes.func.isRequired,
|
|
63
|
+
onBlur: PropTypes.func,
|
|
64
|
+
onClick: PropTypes.func,
|
|
65
|
+
onEdit: PropTypes.func,
|
|
66
|
+
onDelete: PropTypes.func,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Default properties
|
|
71
|
+
* @property {Object} defaultProps Default properties.
|
|
72
|
+
* @static
|
|
73
|
+
*/
|
|
74
|
+
static defaultProps = {
|
|
75
|
+
description: null,
|
|
76
|
+
required: false,
|
|
77
|
+
filterChoices: null,
|
|
78
|
+
items: {
|
|
79
|
+
vocabulary: null,
|
|
80
|
+
},
|
|
81
|
+
widgetOptions: {
|
|
82
|
+
vocabulary: null,
|
|
83
|
+
},
|
|
84
|
+
error: [],
|
|
85
|
+
choices: [],
|
|
86
|
+
value: null,
|
|
87
|
+
onChange: () => {},
|
|
88
|
+
onBlur: () => {},
|
|
89
|
+
onClick: () => {},
|
|
90
|
+
onEdit: null,
|
|
91
|
+
onDelete: null,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Component did mount
|
|
96
|
+
* @method componentDidMount
|
|
97
|
+
* @returns {undefined}
|
|
98
|
+
*/
|
|
99
|
+
componentDidMount() {
|
|
100
|
+
if (
|
|
101
|
+
(!this.props.choices || this.props.choices?.length === 0) &&
|
|
102
|
+
this.props.vocabBaseUrl
|
|
103
|
+
) {
|
|
104
|
+
this.props.getVocabulary({
|
|
105
|
+
vocabNameOrURL: this.props.vocabBaseUrl,
|
|
106
|
+
size: -1,
|
|
107
|
+
subrequest: this.props.lang,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
componentDidUpdate(prevProps) {
|
|
113
|
+
if (
|
|
114
|
+
this.props.vocabBaseUrl !== prevProps.vocabBaseUrl &&
|
|
115
|
+
(!this.props.choices || this.props.choices?.length === 0)
|
|
116
|
+
) {
|
|
117
|
+
this.props.getVocabulary({
|
|
118
|
+
vocabNameOrURL: this.props.vocabBaseUrl,
|
|
119
|
+
size: -1,
|
|
120
|
+
subrequest: this.props.lang,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render method.
|
|
127
|
+
* @method render
|
|
128
|
+
* @returns {string} Markup for the component.
|
|
129
|
+
*/
|
|
130
|
+
render() {
|
|
131
|
+
const { id, choices, value, onChange, filterChoices, additionalChoices } =
|
|
132
|
+
this.props;
|
|
133
|
+
|
|
134
|
+
let options = this.props.vocabBaseUrl
|
|
135
|
+
? this.props.choices
|
|
136
|
+
: [
|
|
137
|
+
...map(choices, (option) => ({
|
|
138
|
+
value: option[0],
|
|
139
|
+
label:
|
|
140
|
+
// Fix "None" on the serializer, to remove when fixed in p.restapi
|
|
141
|
+
option[1] !== 'None' && option[1] ? option[1] : option[0],
|
|
142
|
+
})),
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
if (additionalChoices) {
|
|
146
|
+
options = [...(options || []), ...additionalChoices];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (filterChoices) {
|
|
150
|
+
options = filter(options, (item) => filterChoices.includes(item.value));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<FormFieldWrapper {...this.props}>
|
|
155
|
+
{options.map((option) => (
|
|
156
|
+
<Checkbox
|
|
157
|
+
label={option.label}
|
|
158
|
+
key={option.value}
|
|
159
|
+
name={id}
|
|
160
|
+
value={option.value}
|
|
161
|
+
checked={includes(value, option.value)}
|
|
162
|
+
onChange={(e, data) => {
|
|
163
|
+
const newValue = value || [];
|
|
164
|
+
if (data.checked) {
|
|
165
|
+
onChange(id, [...newValue, data.value]);
|
|
166
|
+
} else {
|
|
167
|
+
onChange(id, without(newValue, data.value));
|
|
168
|
+
}
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
))}
|
|
172
|
+
</FormFieldWrapper>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const CheckboxGroupWidgetComponent = injectIntl(CheckboxGroupWidget);
|
|
178
|
+
|
|
179
|
+
export default connect(
|
|
180
|
+
(state, props) => {
|
|
181
|
+
const vocabBaseUrl = !props.choices
|
|
182
|
+
? getVocabFromHint(props) ||
|
|
183
|
+
getVocabFromField(props) ||
|
|
184
|
+
getVocabFromItems(props)
|
|
185
|
+
: '';
|
|
186
|
+
|
|
187
|
+
const vocabState =
|
|
188
|
+
state.vocabularies?.[vocabBaseUrl]?.subrequests?.[state.intl.locale];
|
|
189
|
+
|
|
190
|
+
// If the schema already has the choices in it, then do not try to get the vocab,
|
|
191
|
+
// even if there is one
|
|
192
|
+
if (props.choices) {
|
|
193
|
+
return {
|
|
194
|
+
choices: props.choices,
|
|
195
|
+
lang: state.intl.locale,
|
|
196
|
+
};
|
|
197
|
+
} else if (vocabState) {
|
|
198
|
+
return {
|
|
199
|
+
vocabBaseUrl,
|
|
200
|
+
choices: vocabState?.items ?? [],
|
|
201
|
+
lang: state.intl.locale,
|
|
202
|
+
};
|
|
203
|
+
// There is a moment that vocabState is not there yet, so we need to pass the
|
|
204
|
+
// vocabBaseUrl to the component.
|
|
205
|
+
} else if (vocabBaseUrl) {
|
|
206
|
+
return {
|
|
207
|
+
vocabBaseUrl,
|
|
208
|
+
lang: state.intl.locale,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { lang: state.intl.locale };
|
|
212
|
+
},
|
|
213
|
+
{ getVocabulary, getVocabularyTokenTitle },
|
|
214
|
+
)(CheckboxGroupWidgetComponent);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import CheckboxGroupWidget, {
|
|
3
|
+
CheckboxGroupWidgetComponent,
|
|
4
|
+
} from './CheckboxGroupWidget';
|
|
5
|
+
import WidgetStory from './story';
|
|
6
|
+
|
|
7
|
+
export const Default = WidgetStory.bind({
|
|
8
|
+
widget: CheckboxGroupWidget,
|
|
9
|
+
});
|
|
10
|
+
Default.args = {
|
|
11
|
+
id: 'field-empty',
|
|
12
|
+
title: 'field 1 title',
|
|
13
|
+
description: 'Optional help text',
|
|
14
|
+
choices: [
|
|
15
|
+
['Foo', 'Foo'],
|
|
16
|
+
['Bar', 'Bar'],
|
|
17
|
+
['FooBar', 'FooBar'],
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
title: 'Edit Widgets/Checkbox Group Widget',
|
|
23
|
+
component: CheckboxGroupWidgetComponent,
|
|
24
|
+
decorators: [
|
|
25
|
+
(Story) => (
|
|
26
|
+
<div style={{ width: '400px' }}>
|
|
27
|
+
<Story />
|
|
28
|
+
</div>
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
argTypes: {
|
|
32
|
+
// controlled value prop
|
|
33
|
+
value: {
|
|
34
|
+
control: {
|
|
35
|
+
disable: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import configureStore from 'redux-mock-store';
|
|
3
|
+
import { Provider } from 'react-intl-redux';
|
|
4
|
+
import { waitFor, render, screen } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
import CheckboxGroupWidget from './CheckboxGroupWidget';
|
|
7
|
+
|
|
8
|
+
const mockStore = configureStore();
|
|
9
|
+
|
|
10
|
+
jest.mock('@plone/volto/helpers/Loadable/Loadable');
|
|
11
|
+
beforeAll(
|
|
12
|
+
async () =>
|
|
13
|
+
await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
test('renders a checkbox group widget component', async () => {
|
|
17
|
+
const store = mockStore({
|
|
18
|
+
intl: {
|
|
19
|
+
locale: 'en',
|
|
20
|
+
messages: {},
|
|
21
|
+
},
|
|
22
|
+
vocabularies: {
|
|
23
|
+
'plone.app.vocabularies.Keywords': {
|
|
24
|
+
items: [{ title: 'My item', value: 'myitem' }],
|
|
25
|
+
itemsTotal: 1,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { container } = render(
|
|
31
|
+
<Provider store={store}>
|
|
32
|
+
<CheckboxGroupWidget
|
|
33
|
+
id="my-field"
|
|
34
|
+
title="My field"
|
|
35
|
+
fieldSet="default"
|
|
36
|
+
onChange={() => {}}
|
|
37
|
+
onBlur={() => {}}
|
|
38
|
+
onClick={() => {}}
|
|
39
|
+
/>
|
|
40
|
+
</Provider>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
await waitFor(() => screen.getByText('My field'));
|
|
44
|
+
expect(container).toMatchSnapshot();
|
|
45
|
+
});
|
|
@@ -10,12 +10,14 @@ import { readAsDataURL } from 'promise-file-reader';
|
|
|
10
10
|
import { injectIntl } from 'react-intl';
|
|
11
11
|
import deleteSVG from '@plone/volto/icons/delete.svg';
|
|
12
12
|
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
13
|
+
import Toast from '@plone/volto/components/manage/Toast/Toast';
|
|
13
14
|
import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
|
|
14
15
|
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
15
16
|
import loadable from '@loadable/component';
|
|
16
17
|
import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
17
18
|
import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
|
|
18
19
|
import { defineMessages, useIntl } from 'react-intl';
|
|
20
|
+
import { toast } from 'react-toastify';
|
|
19
21
|
|
|
20
22
|
const imageMimetypes = [
|
|
21
23
|
'image/png',
|
|
@@ -48,6 +50,15 @@ const messages = defineMessages({
|
|
|
48
50
|
id: 'Choose a file',
|
|
49
51
|
defaultMessage: 'Choose a file',
|
|
50
52
|
},
|
|
53
|
+
maxSizeError: {
|
|
54
|
+
id: 'The file you uploaded exceeded the maximum allowed size of {size} bytes',
|
|
55
|
+
defaultMessage:
|
|
56
|
+
'The file you uploaded exceeded the maximum allowed size of {size} bytes',
|
|
57
|
+
},
|
|
58
|
+
acceptError: {
|
|
59
|
+
id: 'File is not of the accepted type {accept}',
|
|
60
|
+
defaultMessage: 'File is not of the accepted type {accept}',
|
|
61
|
+
},
|
|
51
62
|
});
|
|
52
63
|
|
|
53
64
|
/**
|
|
@@ -96,7 +107,33 @@ const FileWidget = (props) => {
|
|
|
96
107
|
* @param {array} files File objects
|
|
97
108
|
* @returns {undefined}
|
|
98
109
|
*/
|
|
99
|
-
const onDrop = (files) => {
|
|
110
|
+
const onDrop = (files, rejectedFiles) => {
|
|
111
|
+
rejectedFiles.forEach((file) => {
|
|
112
|
+
file.errors.forEach((err) => {
|
|
113
|
+
if (err.code === 'file-too-large') {
|
|
114
|
+
toast.error(
|
|
115
|
+
<Toast
|
|
116
|
+
error
|
|
117
|
+
title={intl.formatMessage(messages.maxSizeError, {
|
|
118
|
+
size: props.size,
|
|
119
|
+
})}
|
|
120
|
+
/>,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (err.code === 'file-invalid-type') {
|
|
125
|
+
toast.error(
|
|
126
|
+
<Toast
|
|
127
|
+
error
|
|
128
|
+
title={intl.formatMessage(messages.acceptError, {
|
|
129
|
+
accept: props.accept,
|
|
130
|
+
})}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
if (files.length < 1) return;
|
|
100
137
|
const file = files[0];
|
|
101
138
|
if (!validateFileUploadSize(file, intl.formatMessage)) return;
|
|
102
139
|
readAsDataURL(file).then((data) => {
|
|
@@ -125,7 +162,11 @@ const FileWidget = (props) => {
|
|
|
125
162
|
|
|
126
163
|
return (
|
|
127
164
|
<FormFieldWrapper {...props}>
|
|
128
|
-
<Dropzone
|
|
165
|
+
<Dropzone
|
|
166
|
+
onDrop={onDrop}
|
|
167
|
+
{...(props.size ? { maxSize: props.size } : {})}
|
|
168
|
+
{...(props.accept ? { accept: props.accept } : {})}
|
|
169
|
+
>
|
|
129
170
|
{({ getRootProps, getInputProps, isDragActive }) => (
|
|
130
171
|
<div className="file-widget-dropzone" {...getRootProps()}>
|
|
131
172
|
{isDragActive && <Dimmer active></Dimmer>}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Input } from 'semantic-ui-react';
|
|
4
|
+
|
|
5
|
+
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
6
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
7
|
+
|
|
8
|
+
const HiddenWidget = (props) => {
|
|
9
|
+
const {
|
|
10
|
+
id,
|
|
11
|
+
value,
|
|
12
|
+
onChange,
|
|
13
|
+
onBlur,
|
|
14
|
+
onClick,
|
|
15
|
+
icon,
|
|
16
|
+
iconAction,
|
|
17
|
+
minLength,
|
|
18
|
+
maxLength,
|
|
19
|
+
placeholder,
|
|
20
|
+
isDisabled,
|
|
21
|
+
focus,
|
|
22
|
+
onEdit,
|
|
23
|
+
} = props;
|
|
24
|
+
|
|
25
|
+
const ref = useRef();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (focus) {
|
|
29
|
+
ref.current.focus();
|
|
30
|
+
}
|
|
31
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return onEdit ? (
|
|
35
|
+
<FormFieldWrapper {...props} className="text">
|
|
36
|
+
<Input
|
|
37
|
+
id={`field-${id}`}
|
|
38
|
+
name={id}
|
|
39
|
+
value={value || ''}
|
|
40
|
+
disabled={isDisabled}
|
|
41
|
+
icon={icon || null}
|
|
42
|
+
placeholder={placeholder}
|
|
43
|
+
onChange={({ target }) =>
|
|
44
|
+
onChange(id, target.value === '' ? undefined : target.value)
|
|
45
|
+
}
|
|
46
|
+
ref={ref}
|
|
47
|
+
onBlur={({ target }) =>
|
|
48
|
+
onBlur(id, target.value === '' ? undefined : target.value)
|
|
49
|
+
}
|
|
50
|
+
onClick={() => onClick()}
|
|
51
|
+
minLength={minLength || null}
|
|
52
|
+
maxLength={maxLength || null}
|
|
53
|
+
/>
|
|
54
|
+
{icon && iconAction && (
|
|
55
|
+
<button className={`field-${id}-action-button`} onClick={iconAction}>
|
|
56
|
+
<Icon name={icon} size="18px" />
|
|
57
|
+
</button>
|
|
58
|
+
)}
|
|
59
|
+
</FormFieldWrapper>
|
|
60
|
+
) : (
|
|
61
|
+
<input
|
|
62
|
+
id={`field-${id}`}
|
|
63
|
+
name={id}
|
|
64
|
+
value={value || ''}
|
|
65
|
+
placeholder={placeholder}
|
|
66
|
+
ref={ref}
|
|
67
|
+
type="hidden"
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default HiddenWidget;
|
|
73
|
+
|
|
74
|
+
HiddenWidget.propTypes = {
|
|
75
|
+
id: PropTypes.string.isRequired,
|
|
76
|
+
title: PropTypes.string.isRequired,
|
|
77
|
+
description: PropTypes.string,
|
|
78
|
+
required: PropTypes.bool,
|
|
79
|
+
error: PropTypes.arrayOf(PropTypes.string),
|
|
80
|
+
value: PropTypes.string,
|
|
81
|
+
focus: PropTypes.bool,
|
|
82
|
+
onChange: PropTypes.func,
|
|
83
|
+
onBlur: PropTypes.func,
|
|
84
|
+
onClick: PropTypes.func,
|
|
85
|
+
onEdit: PropTypes.func,
|
|
86
|
+
onDelete: PropTypes.func,
|
|
87
|
+
icon: PropTypes.shape({
|
|
88
|
+
xmlns: PropTypes.string,
|
|
89
|
+
viewBox: PropTypes.string,
|
|
90
|
+
content: PropTypes.string,
|
|
91
|
+
}),
|
|
92
|
+
iconAction: PropTypes.func,
|
|
93
|
+
minLength: PropTypes.number,
|
|
94
|
+
maxLength: PropTypes.number,
|
|
95
|
+
wrapped: PropTypes.bool,
|
|
96
|
+
placeholder: PropTypes.string,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
HiddenWidget.defaultProps = {
|
|
100
|
+
description: null,
|
|
101
|
+
required: false,
|
|
102
|
+
error: [],
|
|
103
|
+
value: null,
|
|
104
|
+
onChange: () => {},
|
|
105
|
+
onBlur: () => {},
|
|
106
|
+
onClick: () => {},
|
|
107
|
+
onEdit: null,
|
|
108
|
+
onDelete: null,
|
|
109
|
+
focus: false,
|
|
110
|
+
icon: null,
|
|
111
|
+
iconAction: null,
|
|
112
|
+
minLength: null,
|
|
113
|
+
maxLength: null,
|
|
114
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import HiddenWidget from './HiddenWidget';
|
|
2
|
+
import WidgetStory from './story';
|
|
3
|
+
|
|
4
|
+
export const Hidden = WidgetStory.bind({
|
|
5
|
+
props: { id: 'text', title: 'Text' },
|
|
6
|
+
widget: HiddenWidget,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
title: 'Edit Widgets/Hidden',
|
|
11
|
+
component: HiddenWidget,
|
|
12
|
+
decorators: [
|
|
13
|
+
(Story) => (
|
|
14
|
+
<div className="ui segment form attached" style={{ width: '400px' }}>
|
|
15
|
+
<Story />
|
|
16
|
+
</div>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
argTypes: {},
|
|
20
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderer from 'react-test-renderer';
|
|
3
|
+
import configureStore from 'redux-mock-store';
|
|
4
|
+
import { Provider } from 'react-intl-redux';
|
|
5
|
+
|
|
6
|
+
import HiddenWidget from './HiddenWidget';
|
|
7
|
+
|
|
8
|
+
const mockStore = configureStore();
|
|
9
|
+
|
|
10
|
+
test('renders a hidden widget component', () => {
|
|
11
|
+
const store = mockStore({
|
|
12
|
+
intl: {
|
|
13
|
+
locale: 'en',
|
|
14
|
+
messages: {},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const component = renderer.create(
|
|
19
|
+
<Provider store={store}>
|
|
20
|
+
<HiddenWidget
|
|
21
|
+
id="my-field"
|
|
22
|
+
title="My field"
|
|
23
|
+
fieldSet="default"
|
|
24
|
+
onChange={() => {}}
|
|
25
|
+
onBlur={() => {}}
|
|
26
|
+
onClick={() => {}}
|
|
27
|
+
/>
|
|
28
|
+
</Provider>,
|
|
29
|
+
);
|
|
30
|
+
const json = component.toJSON();
|
|
31
|
+
expect(json).toMatchSnapshot();
|
|
32
|
+
});
|
|
@@ -227,6 +227,7 @@ const UnconnectedImageInput = (props) => {
|
|
|
227
227
|
currentPath: contextUrl,
|
|
228
228
|
});
|
|
229
229
|
}}
|
|
230
|
+
type="button"
|
|
230
231
|
>
|
|
231
232
|
<Icon name={navTreeSVG} size="24px" />
|
|
232
233
|
</Button>
|
|
@@ -242,6 +243,7 @@ const UnconnectedImageInput = (props) => {
|
|
|
242
243
|
onClick={() => {
|
|
243
244
|
imageUploadInputRef.current.click();
|
|
244
245
|
}}
|
|
246
|
+
type="button"
|
|
245
247
|
>
|
|
246
248
|
<Icon name={uploadSVG} size="24px" />
|
|
247
249
|
</Button>
|
|
@@ -266,6 +268,7 @@ const UnconnectedImageInput = (props) => {
|
|
|
266
268
|
!props.selected && onFocus && onFocus();
|
|
267
269
|
linkEditor.show();
|
|
268
270
|
}}
|
|
271
|
+
type="button"
|
|
269
272
|
>
|
|
270
273
|
<Icon name={linkSVG} circled size="24px" />
|
|
271
274
|
</Button>
|