@plone/volto 16.22.1 → 16.23.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.draft +8 -15
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +91 -1
- package/CONTRIBUTING.md +5 -1
- package/README.md +6 -0
- package/locales/es/LC_MESSAGES/volto.po +10 -10
- package/locales/es.json +1 -1
- package/package.json +1 -1
- package/packages/volto-slate/build/messages/src/blocks/Table/TableBlockEdit.json +90 -0
- package/packages/volto-slate/build/messages/src/blocks/Text/DefaultTextBlockEditor.json +6 -0
- package/packages/volto-slate/build/messages/src/blocks/Text/DetachedTextBlockEditor.json +6 -0
- package/packages/volto-slate/build/messages/src/blocks/Text/SlashMenu.json +6 -0
- package/packages/volto-slate/build/messages/src/editor/plugins/AdvancedLink/index.json +10 -0
- package/packages/volto-slate/build/messages/src/editor/plugins/Link/index.json +10 -0
- package/packages/volto-slate/build/messages/src/editor/plugins/Table/index.json +30 -0
- package/packages/volto-slate/build/messages/src/elementEditor/messages.json +10 -0
- package/packages/volto-slate/build/messages/src/widgets/HtmlSlateWidget.json +6 -0
- package/packages/volto-slate/build/messages/src/widgets/RichTextWidgetView.json +6 -0
- package/packages/volto-slate/package.json +1 -1
- package/pyvenv.cfg +3 -0
- package/share/man/man1/ttx.1 +225 -0
- package/src/components/manage/Blocks/Block/Settings.jsx +2 -0
- package/src/components/manage/Blocks/Block/Settings.test.jsx +90 -0
- package/src/components/manage/Blocks/Image/View.jsx +2 -1
- package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +18 -11
- package/src/components/manage/Contents/Contents.jsx +27 -0
- package/src/components/manage/Controlpanels/Rules/AddRule.jsx +1 -1
- package/src/components/manage/Controlpanels/Rules/EditRule.jsx +1 -1
- package/src/components/manage/Diff/DiffField.jsx +25 -1
- package/src/components/manage/Form/BlockDataForm.jsx +3 -2
- package/src/components/manage/Form/BlockDataForm.test.jsx +34 -2
- package/src/components/manage/Sharing/Sharing.jsx +11 -5
- package/src/components/manage/Widgets/ArrayWidget.jsx +3 -1
- package/src/components/manage/Widgets/ArrayWidget.test.jsx +45 -1
- package/src/components/manage/Widgets/FormFieldWrapper.jsx +1 -1
- package/src/components/manage/Widgets/SelectWidget.jsx +15 -1
- package/src/components/manage/Widgets/SelectWidget.test.jsx +45 -1
- package/src/components/theme/Sitemap/Sitemap.jsx +24 -13
- package/src/components/theme/Sitemap/Sitemap.test.jsx +23 -2
- package/styles/Vocab/Plone/accept.txt +9 -2
- package/styles/Vocab/Plone/reject.txt +2 -5
- package/theme/themes/pastanaga/collections/form.overrides +46 -0
- package/theme/themes/pastanaga/elements/input.overrides +6 -0
- package/theme/themes/pastanaga/elements/label.overrides +10 -0
- package/.gitignore~ +0 -71
- package/locales/volto.pot~ +0 -4705
- package/news/4547.breaking~ +0 -1
- package/package.json~ +0 -444
- package/src/config/index.js~ +0 -223
|
@@ -1334,6 +1334,9 @@ class Contents extends Component {
|
|
|
1334
1334
|
as={Button}
|
|
1335
1335
|
onClick={this.upload}
|
|
1336
1336
|
className="upload"
|
|
1337
|
+
aria-label={this.props.intl.formatMessage(
|
|
1338
|
+
messages.upload,
|
|
1339
|
+
)}
|
|
1337
1340
|
>
|
|
1338
1341
|
<Icon
|
|
1339
1342
|
name={uploadSVG}
|
|
@@ -1358,6 +1361,9 @@ class Contents extends Component {
|
|
|
1358
1361
|
as={Button}
|
|
1359
1362
|
onClick={this.rename}
|
|
1360
1363
|
disabled={!selected}
|
|
1364
|
+
aria-label={this.props.intl.formatMessage(
|
|
1365
|
+
messages.rename,
|
|
1366
|
+
)}
|
|
1361
1367
|
>
|
|
1362
1368
|
<Icon
|
|
1363
1369
|
name={renameSVG}
|
|
@@ -1380,6 +1386,9 @@ class Contents extends Component {
|
|
|
1380
1386
|
as={Button}
|
|
1381
1387
|
onClick={this.workflow}
|
|
1382
1388
|
disabled={!selected}
|
|
1389
|
+
aria-label={this.props.intl.formatMessage(
|
|
1390
|
+
messages.state,
|
|
1391
|
+
)}
|
|
1383
1392
|
>
|
|
1384
1393
|
<Icon
|
|
1385
1394
|
name={semaphoreSVG}
|
|
@@ -1402,6 +1411,9 @@ class Contents extends Component {
|
|
|
1402
1411
|
as={Button}
|
|
1403
1412
|
onClick={this.tags}
|
|
1404
1413
|
disabled={!selected}
|
|
1414
|
+
aria-label={this.props.intl.formatMessage(
|
|
1415
|
+
messages.tags,
|
|
1416
|
+
)}
|
|
1405
1417
|
>
|
|
1406
1418
|
<Icon
|
|
1407
1419
|
name={tagSVG}
|
|
@@ -1425,6 +1437,9 @@ class Contents extends Component {
|
|
|
1425
1437
|
as={Button}
|
|
1426
1438
|
onClick={this.properties}
|
|
1427
1439
|
disabled={!selected}
|
|
1440
|
+
aria-label={this.props.intl.formatMessage(
|
|
1441
|
+
messages.properties,
|
|
1442
|
+
)}
|
|
1428
1443
|
>
|
|
1429
1444
|
<Icon
|
|
1430
1445
|
name={propertiesSVG}
|
|
@@ -1449,6 +1464,9 @@ class Contents extends Component {
|
|
|
1449
1464
|
as={Button}
|
|
1450
1465
|
onClick={this.cut}
|
|
1451
1466
|
disabled={!selected}
|
|
1467
|
+
aria-label={this.props.intl.formatMessage(
|
|
1468
|
+
messages.cut,
|
|
1469
|
+
)}
|
|
1452
1470
|
>
|
|
1453
1471
|
<Icon
|
|
1454
1472
|
name={cutSVG}
|
|
@@ -1471,6 +1489,9 @@ class Contents extends Component {
|
|
|
1471
1489
|
as={Button}
|
|
1472
1490
|
onClick={this.copy}
|
|
1473
1491
|
disabled={!selected}
|
|
1492
|
+
aria-label={this.props.intl.formatMessage(
|
|
1493
|
+
messages.copy,
|
|
1494
|
+
)}
|
|
1474
1495
|
>
|
|
1475
1496
|
<Icon
|
|
1476
1497
|
name={copySVG}
|
|
@@ -1494,6 +1515,9 @@ class Contents extends Component {
|
|
|
1494
1515
|
as={Button}
|
|
1495
1516
|
onClick={this.paste}
|
|
1496
1517
|
disabled={!this.props.action}
|
|
1518
|
+
aria-label={this.props.intl.formatMessage(
|
|
1519
|
+
messages.paste,
|
|
1520
|
+
)}
|
|
1497
1521
|
>
|
|
1498
1522
|
<Icon
|
|
1499
1523
|
name={pasteSVG}
|
|
@@ -1517,6 +1541,9 @@ class Contents extends Component {
|
|
|
1517
1541
|
as={Button}
|
|
1518
1542
|
onClick={this.delete}
|
|
1519
1543
|
disabled={!selected}
|
|
1544
|
+
aria-label={this.props.intl.formatMessage(
|
|
1545
|
+
messages.delete,
|
|
1546
|
+
)}
|
|
1520
1547
|
>
|
|
1521
1548
|
<Icon
|
|
1522
1549
|
name={deleteSVG}
|
|
@@ -179,7 +179,7 @@ class AddRule extends Component {
|
|
|
179
179
|
const { title, description, event, cascading, stop, enabled } = this.state;
|
|
180
180
|
const triggeringEvents =
|
|
181
181
|
this.props.events?.items && this.props.events?.items.length > 0
|
|
182
|
-
? this.props.events?.items.map((event) => [event.
|
|
182
|
+
? this.props.events?.items.map((event) => [event.token, event.title])
|
|
183
183
|
: '';
|
|
184
184
|
|
|
185
185
|
return (
|
|
@@ -209,7 +209,7 @@ class EditRule extends Component {
|
|
|
209
209
|
|
|
210
210
|
const triggeringEvents =
|
|
211
211
|
this.props.events?.items && this.props.events?.items.length > 0
|
|
212
|
-
? this.props.events?.items.map((event) => [event.
|
|
212
|
+
? this.props.events?.items.map((event) => [event.token, event.title])
|
|
213
213
|
: '';
|
|
214
214
|
|
|
215
215
|
return (
|
|
@@ -17,6 +17,7 @@ import { useSelector } from 'react-redux';
|
|
|
17
17
|
import { Api } from '@plone/volto/helpers';
|
|
18
18
|
import configureStore from '@plone/volto/store';
|
|
19
19
|
import { DefaultView } from '@plone/volto/components/';
|
|
20
|
+
import { serializeNodes } from '@plone/volto-slate/editor/render';
|
|
20
21
|
|
|
21
22
|
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
22
23
|
|
|
@@ -69,7 +70,7 @@ const DiffField = ({
|
|
|
69
70
|
.replace('\u202F', ' '),
|
|
70
71
|
);
|
|
71
72
|
break;
|
|
72
|
-
case 'json':
|
|
73
|
+
case 'json': {
|
|
73
74
|
const api = new Api();
|
|
74
75
|
const history = createBrowserHistory();
|
|
75
76
|
const store = configureStore(window.__data, history, api);
|
|
@@ -90,6 +91,29 @@ const DiffField = ({
|
|
|
90
91
|
),
|
|
91
92
|
);
|
|
92
93
|
break;
|
|
94
|
+
}
|
|
95
|
+
case 'slate': {
|
|
96
|
+
const api = new Api();
|
|
97
|
+
const history = createBrowserHistory();
|
|
98
|
+
const store = configureStore(window.__data, history, api);
|
|
99
|
+
parts = diffWords(
|
|
100
|
+
ReactDOMServer.renderToStaticMarkup(
|
|
101
|
+
<Provider store={store}>
|
|
102
|
+
<ConnectedRouter history={history}>
|
|
103
|
+
{serializeNodes(one)}
|
|
104
|
+
</ConnectedRouter>
|
|
105
|
+
</Provider>,
|
|
106
|
+
),
|
|
107
|
+
ReactDOMServer.renderToStaticMarkup(
|
|
108
|
+
<Provider store={store}>
|
|
109
|
+
<ConnectedRouter history={history}>
|
|
110
|
+
{serializeNodes(two)}
|
|
111
|
+
</ConnectedRouter>
|
|
112
|
+
</Provider>,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
93
117
|
case 'textarea':
|
|
94
118
|
default:
|
|
95
119
|
parts = diffWords(one, two);
|
|
@@ -5,7 +5,7 @@ import { withVariationSchemaEnhancer } from '@plone/volto/helpers';
|
|
|
5
5
|
const EnhancedBlockDataForm = withVariationSchemaEnhancer(InlineForm);
|
|
6
6
|
|
|
7
7
|
export default function BlockDataForm(props) {
|
|
8
|
-
const { onChangeBlock, block } = props;
|
|
8
|
+
const { onChangeBlock, block, applySchemaEnhancers = true } = props;
|
|
9
9
|
|
|
10
10
|
if (!onChangeBlock) {
|
|
11
11
|
// eslint-disable-next-line no-console
|
|
@@ -19,8 +19,9 @@ export default function BlockDataForm(props) {
|
|
|
19
19
|
[block, onChangeBlock],
|
|
20
20
|
);
|
|
21
21
|
|
|
22
|
+
const Form = applySchemaEnhancers ? EnhancedBlockDataForm : InlineForm;
|
|
22
23
|
return (
|
|
23
|
-
<
|
|
24
|
+
<Form
|
|
24
25
|
{...props}
|
|
25
26
|
onChangeFormData={onChangeBlock ? onChangeFormData : undefined}
|
|
26
27
|
/>
|
|
@@ -72,7 +72,7 @@ beforeAll(() => {
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
describe('BlockDataForm', () => {
|
|
75
|
-
it('should
|
|
75
|
+
it('should not add variations to schema when unneeded', () => {
|
|
76
76
|
const WrappedBlockDataForm = withStateManagement(BlockDataForm);
|
|
77
77
|
const store = mockStore({
|
|
78
78
|
intl: {
|
|
@@ -103,7 +103,7 @@ describe('BlockDataForm', () => {
|
|
|
103
103
|
expect(testSchema.fieldsets[0].fields).toStrictEqual([]);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it('should
|
|
106
|
+
it('should not add variations when only one variation', () => {
|
|
107
107
|
const WrappedBlockDataForm = withStateManagement(BlockDataForm);
|
|
108
108
|
const store = mockStore({
|
|
109
109
|
intl: {
|
|
@@ -164,4 +164,36 @@ describe('BlockDataForm', () => {
|
|
|
164
164
|
// schema is cloned, not mutated in place
|
|
165
165
|
expect(testSchema.fieldsets[0].fields).toStrictEqual([]);
|
|
166
166
|
});
|
|
167
|
+
|
|
168
|
+
it('should not add variations to schema when explicitly disabled', () => {
|
|
169
|
+
const WrappedBlockDataForm = withStateManagement(BlockDataForm);
|
|
170
|
+
const store = mockStore({
|
|
171
|
+
intl: {
|
|
172
|
+
locale: 'en',
|
|
173
|
+
messages: {},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const testSchema = {
|
|
177
|
+
fieldsets: [{ title: 'Default', id: 'default', fields: [] }],
|
|
178
|
+
properties: {},
|
|
179
|
+
required: [],
|
|
180
|
+
};
|
|
181
|
+
const formData = {
|
|
182
|
+
'@type': 'testBlock',
|
|
183
|
+
};
|
|
184
|
+
const { container } = render(
|
|
185
|
+
<Provider store={store}>
|
|
186
|
+
<WrappedBlockDataForm
|
|
187
|
+
formData={formData}
|
|
188
|
+
schema={testSchema}
|
|
189
|
+
onChangeField={(id, value) => {}}
|
|
190
|
+
applySchemaEnhancers={false}
|
|
191
|
+
/>
|
|
192
|
+
</Provider>,
|
|
193
|
+
);
|
|
194
|
+
expect(container).toMatchSnapshot();
|
|
195
|
+
|
|
196
|
+
// schema is cloned, not mutated in place
|
|
197
|
+
expect(testSchema.fieldsets[0].fields).toStrictEqual([]);
|
|
198
|
+
});
|
|
167
199
|
});
|
|
@@ -246,9 +246,9 @@ class SharingComponent extends Component {
|
|
|
246
246
|
* @returns {undefined}
|
|
247
247
|
*/
|
|
248
248
|
onToggleInherit() {
|
|
249
|
-
this.setState({
|
|
250
|
-
inherit: !
|
|
251
|
-
});
|
|
249
|
+
this.setState((state) => ({
|
|
250
|
+
inherit: !state.inherit,
|
|
251
|
+
}));
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
@@ -404,9 +404,15 @@ class SharingComponent extends Component {
|
|
|
404
404
|
<Segment attached>
|
|
405
405
|
<Form.Field>
|
|
406
406
|
<Checkbox
|
|
407
|
-
|
|
407
|
+
id="inherit-permissions-checkbox"
|
|
408
|
+
name="inherit-permissions-checkbox"
|
|
409
|
+
defaultChecked={this.state.inherit}
|
|
408
410
|
onChange={this.onToggleInherit}
|
|
409
|
-
label={
|
|
411
|
+
label={
|
|
412
|
+
<label htmlFor="inherit-permissions-checkbox">
|
|
413
|
+
{this.props.intl.formatMessage(messages.inherit)}
|
|
414
|
+
</label>
|
|
415
|
+
}
|
|
410
416
|
/>
|
|
411
417
|
</Form.Field>
|
|
412
418
|
<p className="help">
|
|
@@ -325,7 +325,9 @@ class ArrayWidget extends Component {
|
|
|
325
325
|
: this.props.choices
|
|
326
326
|
? [
|
|
327
327
|
...choices,
|
|
328
|
-
...(this.props.noValueOption &&
|
|
328
|
+
...(this.props.noValueOption &&
|
|
329
|
+
(this.props.default === undefined ||
|
|
330
|
+
this.props.default === null)
|
|
329
331
|
? [
|
|
330
332
|
{
|
|
331
333
|
label: this.props.intl.formatMessage(
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import renderer from 'react-test-renderer';
|
|
3
3
|
import configureStore from 'redux-mock-store';
|
|
4
4
|
import { Provider } from 'react-intl-redux';
|
|
5
|
-
import { waitFor } from '@testing-library/react';
|
|
5
|
+
import { waitFor, render, fireEvent } from '@testing-library/react';
|
|
6
6
|
|
|
7
7
|
import ArrayWidget from './ArrayWidget';
|
|
8
8
|
|
|
@@ -41,3 +41,47 @@ test('renders an array widget component', async () => {
|
|
|
41
41
|
await waitFor(() => {});
|
|
42
42
|
expect(component.toJSON()).toMatchSnapshot();
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
test("No 'No value' option when default value is 0", async () => {
|
|
46
|
+
const store = mockStore({
|
|
47
|
+
intl: {
|
|
48
|
+
locale: 'en',
|
|
49
|
+
messages: {},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const choices = [
|
|
54
|
+
['0', 'None'],
|
|
55
|
+
['1', 'One'],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const value = {
|
|
59
|
+
value: '0',
|
|
60
|
+
label: 'None',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const _default = 0;
|
|
64
|
+
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<Provider store={store}>
|
|
67
|
+
<ArrayWidget
|
|
68
|
+
id="my-field"
|
|
69
|
+
title="My field"
|
|
70
|
+
fieldSet="default"
|
|
71
|
+
choices={choices}
|
|
72
|
+
default={_default}
|
|
73
|
+
value={value}
|
|
74
|
+
noValueOption={true}
|
|
75
|
+
onChange={() => {}}
|
|
76
|
+
onBlur={() => {}}
|
|
77
|
+
onClick={() => {}}
|
|
78
|
+
/>
|
|
79
|
+
</Provider>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
fireEvent.mouseDown(
|
|
83
|
+
container.querySelector('.react-select__dropdown-indicator'),
|
|
84
|
+
{ button: 0 },
|
|
85
|
+
);
|
|
86
|
+
expect(container).toMatchSnapshot();
|
|
87
|
+
});
|
|
@@ -96,7 +96,7 @@ class FormFieldWrapper extends Component {
|
|
|
96
96
|
{this.props.children}
|
|
97
97
|
|
|
98
98
|
{map(error, (message) => (
|
|
99
|
-
<Label key={message} basic color="red"
|
|
99
|
+
<Label key={message} basic color="red" className="form-error-label">
|
|
100
100
|
{message}
|
|
101
101
|
</Label>
|
|
102
102
|
))}
|
|
@@ -167,6 +167,19 @@ class SelectWidget extends Component {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
componentDidUpdate(prevProps) {
|
|
171
|
+
if (
|
|
172
|
+
this.props.vocabBaseUrl !== prevProps.vocabBaseUrl &&
|
|
173
|
+
(!this.props.choices || this.props.choices?.length === 0)
|
|
174
|
+
) {
|
|
175
|
+
this.props.getVocabulary({
|
|
176
|
+
vocabNameOrURL: this.props.vocabBaseUrl,
|
|
177
|
+
size: -1,
|
|
178
|
+
subrequest: this.props.lang,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
/**
|
|
171
184
|
* Render method.
|
|
172
185
|
* @method render
|
|
@@ -190,7 +203,8 @@ class SelectWidget extends Component {
|
|
|
190
203
|
})),
|
|
191
204
|
// Only set "no-value" option if there's no default in the field
|
|
192
205
|
// TODO: also if this.props.defaultValue?
|
|
193
|
-
...(this.props.noValueOption &&
|
|
206
|
+
...(this.props.noValueOption &&
|
|
207
|
+
(this.props.default === undefined || this.props.default === null)
|
|
194
208
|
? [
|
|
195
209
|
{
|
|
196
210
|
label: this.props.intl.formatMessage(messages.no_value),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import configureStore from 'redux-mock-store';
|
|
3
3
|
import { Provider } from 'react-intl-redux';
|
|
4
|
-
import { waitFor, render, screen } from '@testing-library/react';
|
|
4
|
+
import { waitFor, render, screen, fireEvent } from '@testing-library/react';
|
|
5
5
|
|
|
6
6
|
import SelectWidget from './SelectWidget';
|
|
7
7
|
|
|
@@ -43,3 +43,47 @@ test('renders a select widget component', async () => {
|
|
|
43
43
|
await waitFor(() => screen.getByText('My field'));
|
|
44
44
|
expect(container).toMatchSnapshot();
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
test("No 'No value' option when default value is 0", async () => {
|
|
48
|
+
const store = mockStore({
|
|
49
|
+
intl: {
|
|
50
|
+
locale: 'en',
|
|
51
|
+
messages: {},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const choices = [
|
|
56
|
+
['0', 'None'],
|
|
57
|
+
['1', 'One'],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const value = {
|
|
61
|
+
value: '0',
|
|
62
|
+
label: 'None',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const _default = 0;
|
|
66
|
+
|
|
67
|
+
const { container } = render(
|
|
68
|
+
<Provider store={store}>
|
|
69
|
+
<SelectWidget
|
|
70
|
+
id="my-field"
|
|
71
|
+
title="My field"
|
|
72
|
+
fieldSet="default"
|
|
73
|
+
choices={choices}
|
|
74
|
+
default={_default}
|
|
75
|
+
value={value}
|
|
76
|
+
onChange={() => {}}
|
|
77
|
+
onBlur={() => {}}
|
|
78
|
+
onClick={() => {}}
|
|
79
|
+
/>
|
|
80
|
+
</Provider>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await waitFor(() => screen.getByText('None'));
|
|
84
|
+
fireEvent.mouseDown(
|
|
85
|
+
container.querySelector('.react-select__dropdown-indicator'),
|
|
86
|
+
{ button: 0 },
|
|
87
|
+
);
|
|
88
|
+
expect(container).toMatchSnapshot();
|
|
89
|
+
});
|
|
@@ -22,6 +22,13 @@ const messages = defineMessages({
|
|
|
22
22
|
defaultMessage: 'Sitemap',
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
export function getSitemapPath(pathname = '', lang) {
|
|
27
|
+
const prefix = pathname.replace(/\/sitemap$/gm, '').replace(/^\//, '');
|
|
28
|
+
const path = prefix || lang || '';
|
|
29
|
+
return path;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* Sitemap class.
|
|
27
34
|
* @class Sitemap
|
|
@@ -39,11 +46,13 @@ class Sitemap extends Component {
|
|
|
39
46
|
|
|
40
47
|
componentDidMount() {
|
|
41
48
|
const { settings } = config;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
|
|
50
|
+
const lang = settings.isMultilingual
|
|
51
|
+
? `${toBackendLang(this.props.lang)}`
|
|
52
|
+
: null;
|
|
53
|
+
|
|
54
|
+
const path = getSitemapPath(this.props.location.pathname, lang);
|
|
55
|
+
this.props.getNavigation(path, 4);
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/**
|
|
@@ -105,15 +114,17 @@ export default compose(
|
|
|
105
114
|
{
|
|
106
115
|
key: 'navigation',
|
|
107
116
|
promise: ({ location, store: { dispatch, getState } }) => {
|
|
117
|
+
if (!__SERVER__) return;
|
|
108
118
|
const { settings } = config;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
|
|
120
|
+
const path = getSitemapPath(
|
|
121
|
+
location.pathname,
|
|
122
|
+
settings.isMultilingual
|
|
123
|
+
? toBackendLang(getState().intl.locale)
|
|
124
|
+
: null,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return dispatch(getNavigation(path, 4));
|
|
117
128
|
},
|
|
118
129
|
},
|
|
119
130
|
]),
|
|
@@ -4,7 +4,7 @@ import configureStore from 'redux-mock-store';
|
|
|
4
4
|
import { Provider } from 'react-intl-redux';
|
|
5
5
|
import { MemoryRouter } from 'react-router-dom';
|
|
6
6
|
|
|
7
|
-
import { __test__ as Sitemap } from './Sitemap';
|
|
7
|
+
import { __test__ as Sitemap, getSitemapPath } from './Sitemap';
|
|
8
8
|
|
|
9
9
|
const mockStore = configureStore();
|
|
10
10
|
|
|
@@ -46,7 +46,7 @@ describe('Sitemap', () => {
|
|
|
46
46
|
const component = renderer.create(
|
|
47
47
|
<Provider store={store}>
|
|
48
48
|
<MemoryRouter>
|
|
49
|
-
<Sitemap />
|
|
49
|
+
<Sitemap location={{ pathname: '/page-1' }} />
|
|
50
50
|
</MemoryRouter>
|
|
51
51
|
</Provider>,
|
|
52
52
|
);
|
|
@@ -54,3 +54,24 @@ describe('Sitemap', () => {
|
|
|
54
54
|
expect(json).toMatchSnapshot();
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
describe('getSitemapPath', () => {
|
|
59
|
+
it('accepts empty path', () => {
|
|
60
|
+
expect(getSitemapPath('', null)).toBe('');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('accepts a path', () => {
|
|
64
|
+
expect(getSitemapPath('/page-1/sitemap', null)).toBe('page-1');
|
|
65
|
+
});
|
|
66
|
+
it('accepts a path', () => {
|
|
67
|
+
expect(getSitemapPath('/page-1/sitemap', null)).toBe('page-1');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('uses a language as default root', () => {
|
|
71
|
+
expect(getSitemapPath('/', 'de')).toBe('de');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts language-rooted paths', () => {
|
|
75
|
+
expect(getSitemapPath('/de/mission', 'de')).toBe('de/mission');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -49,6 +49,52 @@
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
.ui.form .fields.error .field textarea,
|
|
53
|
+
.ui.form .fields.error .field select,
|
|
54
|
+
.ui.form .fields.error .field input:not([type]),
|
|
55
|
+
.ui.form .fields.error .field input[type='date'],
|
|
56
|
+
.ui.form .fields.error .field input[type='datetime-local'],
|
|
57
|
+
.ui.form .fields.error .field input[type='email'],
|
|
58
|
+
.ui.form .fields.error .field input[type='number'],
|
|
59
|
+
.ui.form .fields.error .field input[type='password'],
|
|
60
|
+
.ui.form .fields.error .field input[type='search'],
|
|
61
|
+
.ui.form .fields.error .field input[type='tel'],
|
|
62
|
+
.ui.form .fields.error .field input[type='time'],
|
|
63
|
+
.ui.form .fields.error .field input[type='text'],
|
|
64
|
+
.ui.form .fields.error .field input[type='file'],
|
|
65
|
+
.ui.form .fields.error .field input[type='url'],
|
|
66
|
+
.ui.form .field.error textarea,
|
|
67
|
+
.ui.form .field.error select,
|
|
68
|
+
.ui.form .field.error input:not([type]),
|
|
69
|
+
.ui.form .field.error input[type='date'],
|
|
70
|
+
.ui.form .field.error input[type='datetime-local'],
|
|
71
|
+
.ui.form .field.error input[type='email'],
|
|
72
|
+
.ui.form .field.error input[type='number'],
|
|
73
|
+
.ui.form .field.error input[type='password'],
|
|
74
|
+
.ui.form .field.error input[type='search'],
|
|
75
|
+
.ui.form .field.error input[type='tel'],
|
|
76
|
+
.ui.form .field.error input[type='time'],
|
|
77
|
+
.ui.form .field.error input[type='text'],
|
|
78
|
+
.ui.form .field.error input[type='file'],
|
|
79
|
+
.ui.form .field.error input[type='url'],
|
|
80
|
+
.ui.form .field.error textarea:focus,
|
|
81
|
+
.ui.form .field.error select:focus,
|
|
82
|
+
.ui.form .field.error input:not([type]):focus,
|
|
83
|
+
.ui.form .field.error input[type='date']:focus,
|
|
84
|
+
.ui.form .field.error input[type='datetime-local']:focus,
|
|
85
|
+
.ui.form .field.error input[type='email']:focus,
|
|
86
|
+
.ui.form .field.error input[type='number']:focus,
|
|
87
|
+
.ui.form .field.error input[type='password']:focus,
|
|
88
|
+
.ui.form .field.error input[type='search']:focus,
|
|
89
|
+
.ui.form .field.error input[type='tel']:focus,
|
|
90
|
+
.ui.form .field.error input[type='time']:focus,
|
|
91
|
+
.ui.form .field.error input[type='text']:focus,
|
|
92
|
+
.ui.form .field.error input[type='file']:focus,
|
|
93
|
+
.ui.form .field.error input[type='url']:focus {
|
|
94
|
+
border-color: #d01157;
|
|
95
|
+
background: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
52
98
|
.ui.form .field > .selection.dropdown {
|
|
53
99
|
height: 60px;
|
|
54
100
|
}
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
font-weight: @inputFontWeight;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/* This aligns the height of the field name label to the other side in case
|
|
10
|
+
of an error is present, it overrides a default from SemanticUI grid definitions. */
|
|
11
|
+
.ui.grid > .stretched.row > .column > .wrapper {
|
|
12
|
+
flex-grow: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
.inline.field {
|
|
10
16
|
.wrapper {
|
|
11
17
|
display: flex;
|