@plone/volto 17.0.0-alpha.24 → 17.0.0-alpha.26
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/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +92 -4
- package/CONTRIBUTING.md +5 -1
- package/README.md +9 -7
- package/cypress/support/commands.js +12 -9
- package/cypress.config.js +1 -0
- package/locales/ca/LC_MESSAGES/volto.po +41 -15
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +41 -15
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +40 -14
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +69 -43
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +40 -14
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +40 -14
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +41 -15
- package/locales/fr.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +40 -14
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +40 -14
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +41 -15
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +41 -15
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +40 -14
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +41 -15
- package/locales/ro.json +1 -1
- package/locales/volto.pot +41 -15
- package/locales/zh_CN/LC_MESSAGES/volto.po +41 -15
- package/locales/zh_CN.json +1 -1
- package/package.json +4 -4
- 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/packages/volto-slate/src/editor/render.jsx +2 -3
- package/src/actions/index.js +4 -0
- package/src/actions/navroot/navroot.js +16 -0
- package/src/actions/navroot/navroot.test.js +15 -0
- package/src/actions/relations/relations.js +17 -0
- package/src/actions/site/site.js +16 -0
- package/src/actions/site/site.test.js +15 -0
- package/src/actions/userSession/userSession.js +17 -1
- 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/schema.js +5 -1
- package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +18 -11
- package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +42 -25
- package/src/components/manage/Blocks/ToC/View.jsx +75 -13
- package/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +4 -13
- package/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.test.jsx +44 -0
- package/src/components/manage/Contents/Contents.jsx +27 -0
- package/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx +65 -38
- package/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx +11 -9
- package/src/components/manage/Controlpanels/Relations/Relations.jsx +3 -3
- package/src/components/manage/Controlpanels/Relations/RelationsListing.jsx +8 -7
- package/src/components/manage/Controlpanels/Relations/RelationsMatrix.jsx +15 -9
- package/src/components/manage/Controlpanels/Rules/AddRule.jsx +1 -1
- package/src/components/manage/Controlpanels/Rules/EditRule.jsx +1 -1
- package/src/components/manage/Controlpanels/Users/RenderUsers.jsx +95 -5
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +127 -99
- 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/LinksToItem/LinksToItem.jsx +1 -1
- package/src/components/manage/LinksToItem/LinksToItem.test.jsx +5 -2
- package/src/components/manage/Messages/Messages.jsx +32 -99
- package/src/components/manage/Messages/Messages.test.jsx +0 -1
- package/src/components/manage/Sharing/Sharing.jsx +50 -21
- package/src/components/manage/UniversalLink/UniversalLink.jsx +4 -6
- 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/RegistryImageWidget.jsx +210 -0
- package/src/components/manage/Widgets/RegistryImageWidget.test.jsx +91 -0
- package/src/components/manage/Widgets/SelectWidget.jsx +15 -1
- package/src/components/manage/Widgets/SelectWidget.test.jsx +45 -1
- package/src/components/theme/Comments/Comment.stories.jsx +84 -0
- package/src/components/theme/Comments/Comments.jsx +273 -378
- package/src/components/theme/ContentMetadataTags/ContentMetadataTags.jsx +37 -3
- package/src/components/theme/Login/Login.jsx +159 -241
- package/src/components/theme/Logo/Logo.Multilingual.test.jsx +131 -1
- package/src/components/theme/Logo/Logo.jsx +35 -29
- package/src/components/theme/Logo/Logo.test.jsx +135 -1
- package/src/components/theme/Logout/Logout.jsx +36 -83
- package/src/components/theme/Navigation/Navigation.jsx +86 -171
- package/src/components/theme/Search/SearchTags.jsx +30 -60
- package/src/components/theme/SearchWidget/SearchWidget.jsx +15 -3
- package/src/components/theme/SearchWidget/SearchWidget.test.jsx +8 -0
- package/src/components/theme/Sitemap/Sitemap.jsx +24 -13
- package/src/components/theme/Sitemap/Sitemap.test.jsx +23 -2
- package/src/components/theme/View/View.jsx +2 -0
- package/src/config/ControlPanels.js +0 -1
- package/src/config/Widgets.jsx +2 -0
- package/src/config/index.js +15 -3
- package/src/constants/ActionTypes.js +4 -0
- package/src/express-middleware/images.js +1 -0
- package/src/helpers/MessageLabels/MessageLabels.js +26 -4
- package/src/helpers/Site/index.js +21 -0
- package/src/helpers/index.js +1 -0
- package/src/reducers/index.js +4 -0
- package/src/reducers/navroot/navroot.js +79 -0
- package/src/reducers/navroot/navroot.test.js +110 -0
- package/src/reducers/relations/relations.js +74 -46
- package/src/reducers/site/site.js +51 -0
- package/src/reducers/site/site.test.js +67 -0
- package/src/reducers/userSession/userSession.js +15 -1
- package/src/server.jsx +9 -0
- package/test-setup-config.js +1 -0
- package/theme/themes/pastanaga/collections/form.overrides +46 -0
- package/theme/themes/pastanaga/elements/input.overrides +10 -0
- package/theme/themes/pastanaga/elements/label.overrides +10 -0
- package/theme/themes/pastanaga/extras/login.less +3 -0
- package/webpack-plugins/webpack-less-plugin.js +19 -0
- package/.gitignore~ +0 -71
- package/news/4547.breaking~ +0 -1
- package/package.json~ +0 -444
- package/src/config/index.js~ +0 -223
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getSite } from './site';
|
|
2
|
+
import { GET_SITE } from '@plone/volto/constants/ActionTypes';
|
|
3
|
+
|
|
4
|
+
describe('Site action', () => {
|
|
5
|
+
describe('getSite', () => {
|
|
6
|
+
it('should create an action to get the site config', () => {
|
|
7
|
+
const url = 'http://localhost';
|
|
8
|
+
const action = getSite(url);
|
|
9
|
+
|
|
10
|
+
expect(action.type).toEqual(GET_SITE);
|
|
11
|
+
expect(action.request.op).toEqual('get');
|
|
12
|
+
expect(action.request.path).toEqual(`/@site`);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* @module actions/search/search
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
LOGIN,
|
|
8
|
+
LOGIN_RENEW,
|
|
9
|
+
LOGOUT,
|
|
10
|
+
RESET_LOGIN_REQUEST,
|
|
11
|
+
} from '@plone/volto/constants/ActionTypes';
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Login function.
|
|
@@ -52,3 +57,14 @@ export function logout() {
|
|
|
52
57
|
},
|
|
53
58
|
};
|
|
54
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reset login request data function
|
|
63
|
+
* @function resetLoginRequest
|
|
64
|
+
* @returns {Object} Get content action
|
|
65
|
+
*/
|
|
66
|
+
export function resetLoginRequest() {
|
|
67
|
+
return {
|
|
68
|
+
type: RESET_LOGIN_REQUEST,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Settings from './Settings';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
5
|
+
import configureStore from 'redux-mock-store';
|
|
6
|
+
import config from '@plone/volto/registry';
|
|
7
|
+
import { Provider } from 'react-intl-redux';
|
|
8
|
+
|
|
9
|
+
const mockStore = configureStore();
|
|
10
|
+
|
|
11
|
+
const withStateManagement = (Component) => ({ ...props }) => {
|
|
12
|
+
const [formData, onChangeFormData] = React.useState(props.formData || {});
|
|
13
|
+
const onChangeField = (id, value) => {
|
|
14
|
+
onChangeFormData({ ...formData, [id]: value });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// NOTE: onChangeBlock here is not "really" implemented
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Component
|
|
21
|
+
{...props}
|
|
22
|
+
onChangeField={onChangeField}
|
|
23
|
+
onChangeBlock={(block, data) => onChangeFormData(data)}
|
|
24
|
+
formData={formData}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
config.widgets = {
|
|
31
|
+
id: {},
|
|
32
|
+
widget: {},
|
|
33
|
+
vocabulary: {},
|
|
34
|
+
choices: (props) => (
|
|
35
|
+
<div className="SelectWidget">{JSON.stringify(props.choices)}</div>
|
|
36
|
+
),
|
|
37
|
+
type: {},
|
|
38
|
+
default: () => <div className="TextWidget" />,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
config.blocks.blocksConfig = {
|
|
42
|
+
...config.blocks.blocksConfig,
|
|
43
|
+
|
|
44
|
+
testBlock: {
|
|
45
|
+
id: 'testBlock',
|
|
46
|
+
variations: [
|
|
47
|
+
{
|
|
48
|
+
id: 'default',
|
|
49
|
+
title: 'Default',
|
|
50
|
+
isDefault: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'extra',
|
|
54
|
+
title: 'Extra',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('Settings', () => {
|
|
62
|
+
it('renders block settings form without schema enhancers', () => {
|
|
63
|
+
const WrappedSettings = withStateManagement(Settings);
|
|
64
|
+
const store = mockStore({
|
|
65
|
+
intl: {
|
|
66
|
+
locale: 'en',
|
|
67
|
+
messages: {},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const testSchema = {
|
|
71
|
+
fieldsets: [{ title: 'Default', id: 'default', fields: [] }],
|
|
72
|
+
properties: {},
|
|
73
|
+
required: [],
|
|
74
|
+
};
|
|
75
|
+
const data = {
|
|
76
|
+
'@type': 'testBlock',
|
|
77
|
+
};
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<Provider store={store}>
|
|
80
|
+
<WrappedSettings
|
|
81
|
+
data={data}
|
|
82
|
+
block="test"
|
|
83
|
+
schema={testSchema}
|
|
84
|
+
onChangeBlock={(block, value) => {}}
|
|
85
|
+
/>
|
|
86
|
+
</Provider>,
|
|
87
|
+
);
|
|
88
|
+
expect(container).toMatchSnapshot();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -33,6 +33,10 @@ const messages = defineMessages({
|
|
|
33
33
|
id: 'Alt text hint link text',
|
|
34
34
|
defaultMessage: 'Describe the purpose of the image.',
|
|
35
35
|
},
|
|
36
|
+
linkSettings: {
|
|
37
|
+
id: 'Link settings',
|
|
38
|
+
defaultMessage: 'Link settings',
|
|
39
|
+
},
|
|
36
40
|
});
|
|
37
41
|
|
|
38
42
|
export function ImageSchema({ formData, intl }) {
|
|
@@ -47,7 +51,7 @@ export function ImageSchema({ formData, intl }) {
|
|
|
47
51
|
? [
|
|
48
52
|
{
|
|
49
53
|
id: 'link_settings',
|
|
50
|
-
title:
|
|
54
|
+
title: intl.formatMessage(messages.linkSettings),
|
|
51
55
|
fields: ['href', 'openLinkInNewTab'],
|
|
52
56
|
},
|
|
53
57
|
]
|
|
@@ -23,7 +23,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
23
23
|
} = props;
|
|
24
24
|
const { settings } = config;
|
|
25
25
|
const querystring = data.querystring || data; // For backwards compat with data saved before Blocks schema. Note, this is also how the Search block passes data to ListingBody
|
|
26
|
-
|
|
26
|
+
const subrequestID = content?.UID ? `${content?.UID}-${id}` : id;
|
|
27
27
|
const { b_size = settings.defaultPageSize } = querystring; // batchsize
|
|
28
28
|
|
|
29
29
|
// save the path so it won't trigger dispatch on eager router location change
|
|
@@ -52,31 +52,33 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
52
52
|
|
|
53
53
|
const folderItems = content?.is_folderish ? content.items : [];
|
|
54
54
|
const hasQuery = querystring?.query?.length > 0;
|
|
55
|
-
const hasLoaded = hasQuery
|
|
55
|
+
const hasLoaded = hasQuery
|
|
56
|
+
? querystringResults?.[subrequestID]?.loaded
|
|
57
|
+
: true;
|
|
56
58
|
|
|
57
59
|
const listingItems = hasQuery
|
|
58
|
-
? querystringResults?.[
|
|
60
|
+
? querystringResults?.[subrequestID]?.items || []
|
|
59
61
|
: folderItems;
|
|
60
62
|
|
|
61
63
|
const showAsFolderListing = !hasQuery && content?.items_total > b_size;
|
|
62
64
|
const showAsQueryListing =
|
|
63
|
-
hasQuery && querystringResults?.[
|
|
65
|
+
hasQuery && querystringResults?.[subrequestID]?.total > b_size;
|
|
64
66
|
|
|
65
67
|
const totalPages = showAsFolderListing
|
|
66
68
|
? Math.ceil(content.items_total / b_size)
|
|
67
69
|
: showAsQueryListing
|
|
68
|
-
? Math.ceil(querystringResults[
|
|
70
|
+
? Math.ceil(querystringResults[subrequestID].total / b_size)
|
|
69
71
|
: 0;
|
|
70
72
|
|
|
71
73
|
const prevBatch = showAsFolderListing
|
|
72
74
|
? content.batching?.prev
|
|
73
75
|
: showAsQueryListing
|
|
74
|
-
? querystringResults[
|
|
76
|
+
? querystringResults[subrequestID].batching?.prev
|
|
75
77
|
: null;
|
|
76
78
|
const nextBatch = showAsFolderListing
|
|
77
79
|
? content.batching?.next
|
|
78
80
|
: showAsQueryListing
|
|
79
|
-
? querystringResults[
|
|
81
|
+
? querystringResults[subrequestID].batching?.next
|
|
80
82
|
: null;
|
|
81
83
|
|
|
82
84
|
const isImageGallery =
|
|
@@ -86,7 +88,12 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
86
88
|
useDeepCompareEffect(() => {
|
|
87
89
|
if (hasQuery) {
|
|
88
90
|
dispatch(
|
|
89
|
-
getQueryStringResults(
|
|
91
|
+
getQueryStringResults(
|
|
92
|
+
initialPath,
|
|
93
|
+
adaptedQuery,
|
|
94
|
+
subrequestID,
|
|
95
|
+
currentPage,
|
|
96
|
+
),
|
|
90
97
|
);
|
|
91
98
|
} else if (isImageGallery && !hasQuery) {
|
|
92
99
|
// when used as image gallery, it doesn't need a query to list children
|
|
@@ -104,7 +111,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
104
111
|
},
|
|
105
112
|
],
|
|
106
113
|
},
|
|
107
|
-
|
|
114
|
+
subrequestID,
|
|
108
115
|
),
|
|
109
116
|
);
|
|
110
117
|
} else {
|
|
@@ -113,7 +120,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
113
120
|
adaptedQueryRef.current = adaptedQuery;
|
|
114
121
|
currentPageRef.current = currentPage;
|
|
115
122
|
}, [
|
|
116
|
-
|
|
123
|
+
subrequestID,
|
|
117
124
|
isImageGallery,
|
|
118
125
|
adaptedQuery,
|
|
119
126
|
hasQuery,
|
|
@@ -126,7 +133,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
126
133
|
<WrappedComponent
|
|
127
134
|
{...props}
|
|
128
135
|
onPaginationChange={(e, { activePage }) => setCurrentPage(activePage)}
|
|
129
|
-
total={querystringResults?.[
|
|
136
|
+
total={querystringResults?.[subrequestID]?.total}
|
|
130
137
|
batch_size={b_size}
|
|
131
138
|
currentPage={currentPage}
|
|
132
139
|
totalPages={totalPages}
|
|
@@ -5,6 +5,8 @@ import { useLocation, useHistory } from 'react-router-dom';
|
|
|
5
5
|
|
|
6
6
|
import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
|
|
7
7
|
import config from '@plone/volto/registry';
|
|
8
|
+
import { usePrevious } from '@plone/volto/helpers';
|
|
9
|
+
import { isEqual } from 'lodash';
|
|
8
10
|
|
|
9
11
|
function getDisplayName(WrappedComponent) {
|
|
10
12
|
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
|
@@ -253,35 +255,50 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
253
255
|
const multiFacets = data.facets
|
|
254
256
|
?.filter((facet) => facet?.multiple)
|
|
255
257
|
.map((facet) => facet?.field?.value);
|
|
256
|
-
const [facets, setFacets] = React.useState(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
258
|
+
const [facets, setFacets] = React.useState({});
|
|
259
|
+
const previousUrlQuery = usePrevious(urlQuery);
|
|
260
|
+
|
|
261
|
+
React.useEffect(() => {
|
|
262
|
+
if (!isEqual(urlQuery, previousUrlQuery)) {
|
|
263
|
+
setFacets(
|
|
264
|
+
Object.assign(
|
|
265
|
+
{},
|
|
266
|
+
...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
|
|
267
|
+
|
|
268
|
+
// support for simple filters like ?Subject=something
|
|
269
|
+
// TODO: since the move to hash params this is no longer working.
|
|
270
|
+
// We'd have to treat the location.search and manage it just like the
|
|
271
|
+
// hash, to support it. We can read it, but we'd have to reset it as
|
|
272
|
+
// well, so at that point what's the difference to the hash?
|
|
273
|
+
...configuredFacets.map((f) =>
|
|
274
|
+
locationSearchData[f]
|
|
275
|
+
? {
|
|
276
|
+
[f]:
|
|
277
|
+
multiFacets.indexOf(f) > -1
|
|
278
|
+
? [locationSearchData[f]]
|
|
279
|
+
: locationSearchData[f],
|
|
280
|
+
}
|
|
281
|
+
: {},
|
|
282
|
+
),
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}, [
|
|
287
|
+
urlQuery,
|
|
288
|
+
configuredFacets,
|
|
289
|
+
locationSearchData,
|
|
290
|
+
multiFacets,
|
|
291
|
+
previousUrlQuery,
|
|
292
|
+
]);
|
|
278
293
|
|
|
279
294
|
const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
|
|
280
295
|
const [sortOrder, setSortOrder] = React.useState(data?.query?.sort_order);
|
|
281
296
|
|
|
282
|
-
const [searchData, setSearchData] = React.useState(
|
|
283
|
-
|
|
284
|
-
)
|
|
297
|
+
const [searchData, setSearchData] = React.useState({});
|
|
298
|
+
|
|
299
|
+
React.useEffect(() => {
|
|
300
|
+
setSearchData(getInitialState(data, facets, urlSearchText, id));
|
|
301
|
+
}, [facets, data, urlSearchText, id]);
|
|
285
302
|
|
|
286
303
|
const timeoutRef = React.useRef();
|
|
287
304
|
const facetSettings = data?.facets;
|
|
@@ -16,16 +16,79 @@ import {
|
|
|
16
16
|
getBlocksLayoutFieldname,
|
|
17
17
|
} from '@plone/volto/helpers';
|
|
18
18
|
|
|
19
|
+
export const getBlocksTocEntries = (properties, tocData) => {
|
|
20
|
+
const blocksFieldName = getBlocksFieldname(properties);
|
|
21
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
|
|
22
|
+
|
|
23
|
+
const blocks = properties[blocksFieldName];
|
|
24
|
+
const blocks_layout = properties[blocksLayoutFieldname];
|
|
25
|
+
|
|
26
|
+
const levels =
|
|
27
|
+
tocData.levels?.length > 0
|
|
28
|
+
? tocData.levels.map((l) => parseInt(l.slice(1)))
|
|
29
|
+
: [1, 2, 3, 4, 5, 6];
|
|
30
|
+
let rootLevel = Infinity;
|
|
31
|
+
let blocksFormEntries = [];
|
|
32
|
+
let tocEntries = {};
|
|
33
|
+
let tocEntriesLayout = [];
|
|
34
|
+
|
|
35
|
+
blocks_layout.items.forEach((id) => {
|
|
36
|
+
const block = blocks[id];
|
|
37
|
+
const blockConfig = config.blocks.blocksConfig[block['@type']];
|
|
38
|
+
|
|
39
|
+
if (!block || !blockConfig) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (!blockConfig.tocEntries && !blockConfig.tocEntry) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const blockTocEntry = blockConfig.tocEntry?.(block, tocData);
|
|
47
|
+
|
|
48
|
+
const blockTocEntries = [
|
|
49
|
+
...(blockConfig.tocEntries?.(block, tocData) ||
|
|
50
|
+
(blockTocEntry ? [blockTocEntry] : [])),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
blocksFormEntries = [...blocksFormEntries, ...blockTocEntries];
|
|
54
|
+
|
|
55
|
+
blockTocEntries.forEach((entry, index) => {
|
|
56
|
+
const i = `${id}-${index}`;
|
|
57
|
+
const level = entry[0];
|
|
58
|
+
const title = entry[1];
|
|
59
|
+
const items = [];
|
|
60
|
+
if (!level || !levels.includes(level)) return;
|
|
61
|
+
tocEntriesLayout.push(i);
|
|
62
|
+
tocEntries[i] = {
|
|
63
|
+
level,
|
|
64
|
+
title: title || block.plaintext,
|
|
65
|
+
items,
|
|
66
|
+
id: i,
|
|
67
|
+
};
|
|
68
|
+
if (level < rootLevel) {
|
|
69
|
+
rootLevel = level;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
rootLevel,
|
|
76
|
+
blocksFormEntries,
|
|
77
|
+
tocEntries,
|
|
78
|
+
tocEntriesLayout,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
19
82
|
/**
|
|
20
83
|
* View toc block class.
|
|
21
84
|
* @class View
|
|
22
85
|
* @extends Component
|
|
23
86
|
*/
|
|
24
87
|
const View = (props) => {
|
|
25
|
-
const {
|
|
88
|
+
const { data } = props;
|
|
26
89
|
const { variation } = props;
|
|
27
|
-
const
|
|
28
|
-
const
|
|
90
|
+
const metadata = props.metadata || props.properties;
|
|
91
|
+
const blocksFieldname = getBlocksFieldname(metadata);
|
|
29
92
|
const levels = React.useMemo(
|
|
30
93
|
() =>
|
|
31
94
|
data.levels?.length > 0
|
|
@@ -34,14 +97,15 @@ const View = (props) => {
|
|
|
34
97
|
[data],
|
|
35
98
|
);
|
|
36
99
|
const tocEntries = React.useMemo(() => {
|
|
37
|
-
let rootLevel = Infinity;
|
|
38
100
|
let entries = [];
|
|
39
101
|
let prevEntry = {};
|
|
40
|
-
|
|
41
|
-
|
|
102
|
+
const { rootLevel, tocEntries, tocEntriesLayout } = getBlocksTocEntries(
|
|
103
|
+
metadata,
|
|
104
|
+
data,
|
|
105
|
+
);
|
|
42
106
|
|
|
43
|
-
|
|
44
|
-
const block =
|
|
107
|
+
tocEntriesLayout.forEach((id) => {
|
|
108
|
+
const block = metadata[blocksFieldname][id];
|
|
45
109
|
if (typeof block === 'undefined') {
|
|
46
110
|
return null;
|
|
47
111
|
}
|
|
@@ -50,6 +114,7 @@ const View = (props) => {
|
|
|
50
114
|
block,
|
|
51
115
|
data,
|
|
52
116
|
);
|
|
117
|
+
|
|
53
118
|
if (entry) {
|
|
54
119
|
const level = entry[0];
|
|
55
120
|
const title = entry[1];
|
|
@@ -64,9 +129,6 @@ const View = (props) => {
|
|
|
64
129
|
override_toc: block.override_toc,
|
|
65
130
|
plaintext: block.plaintext,
|
|
66
131
|
};
|
|
67
|
-
if (level < rootLevel) {
|
|
68
|
-
rootLevel = level;
|
|
69
|
-
}
|
|
70
132
|
}
|
|
71
133
|
});
|
|
72
134
|
|
|
@@ -98,7 +160,7 @@ const View = (props) => {
|
|
|
98
160
|
});
|
|
99
161
|
|
|
100
162
|
return entries;
|
|
101
|
-
}, [data, levels,
|
|
163
|
+
}, [data, levels, metadata, blocksFieldname]);
|
|
102
164
|
|
|
103
165
|
const Renderer = variation?.view;
|
|
104
166
|
return (
|
|
@@ -108,7 +170,7 @@ const View = (props) => {
|
|
|
108
170
|
)}
|
|
109
171
|
|
|
110
172
|
{Renderer ? (
|
|
111
|
-
<Renderer {...props} tocEntries={tocEntries}
|
|
173
|
+
<Renderer {...props} tocEntries={tocEntries} metadata={metadata} />
|
|
112
174
|
) : (
|
|
113
175
|
<div>View extension not found</div>
|
|
114
176
|
)}
|
|
@@ -8,11 +8,10 @@ import PropTypes from 'prop-types';
|
|
|
8
8
|
import { map } from 'lodash';
|
|
9
9
|
import { List } from 'semantic-ui-react';
|
|
10
10
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
|
11
|
-
import { useHistory } from 'react-router-dom';
|
|
12
|
-
import AnchorLink from 'react-anchor-link-smooth-scroll';
|
|
13
11
|
import Slugger from 'github-slugger';
|
|
12
|
+
import { UniversalLink } from '@plone/volto/components';
|
|
14
13
|
|
|
15
|
-
const RenderListItems = ({ items, data
|
|
14
|
+
const RenderListItems = ({ items, data }) => {
|
|
16
15
|
return map(items, (item) => {
|
|
17
16
|
const { id, level, title, override_toc, plaintext } = item;
|
|
18
17
|
const slug = override_toc
|
|
@@ -21,14 +20,7 @@ const RenderListItems = ({ items, data, history }) => {
|
|
|
21
20
|
return (
|
|
22
21
|
item && (
|
|
23
22
|
<List.Item key={id} className={`item headline-${level}`} as="li">
|
|
24
|
-
<
|
|
25
|
-
href={`#${slug}`}
|
|
26
|
-
onClick={(e) => {
|
|
27
|
-
history.push({ hash: slug });
|
|
28
|
-
}}
|
|
29
|
-
>
|
|
30
|
-
{title}
|
|
31
|
-
</AnchorLink>
|
|
23
|
+
<UniversalLink href={`#${slug}`}>{title}</UniversalLink>
|
|
32
24
|
{item.items?.length > 0 && (
|
|
33
25
|
<List
|
|
34
26
|
ordered={data.ordered}
|
|
@@ -50,7 +42,6 @@ const RenderListItems = ({ items, data, history }) => {
|
|
|
50
42
|
* @extends Component
|
|
51
43
|
*/
|
|
52
44
|
const View = ({ data, tocEntries }) => {
|
|
53
|
-
const history = useHistory();
|
|
54
45
|
return (
|
|
55
46
|
<>
|
|
56
47
|
{data.title && !data.hide_title ? (
|
|
@@ -70,7 +61,7 @@ const View = ({ data, tocEntries }) => {
|
|
|
70
61
|
bulleted={!data.ordered}
|
|
71
62
|
as={data.ordered ? 'ol' : 'ul'}
|
|
72
63
|
>
|
|
73
|
-
<RenderListItems items={tocEntries} data={data}
|
|
64
|
+
<RenderListItems items={tocEntries} data={data} />
|
|
74
65
|
</List>
|
|
75
66
|
</>
|
|
76
67
|
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import renderer from 'react-test-renderer';
|
|
2
|
+
import configureStore from 'redux-mock-store';
|
|
3
|
+
import { Provider } from 'react-intl-redux';
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
+
import DefaultTocRenderer from './DefaultTocRenderer';
|
|
6
|
+
|
|
7
|
+
const mockStore = configureStore();
|
|
8
|
+
|
|
9
|
+
const data = { '@type': 'toc', variation: 'default' };
|
|
10
|
+
|
|
11
|
+
const tocEntries = [
|
|
12
|
+
{
|
|
13
|
+
level: 2,
|
|
14
|
+
title: 'Hello this is a sample page',
|
|
15
|
+
items: [
|
|
16
|
+
{
|
|
17
|
+
level: 3,
|
|
18
|
+
title: 'Test level 3',
|
|
19
|
+
items: [],
|
|
20
|
+
id: 'be612682-6df9-4a5e-b3a1-9dec5d82ae14',
|
|
21
|
+
parentId: '3a8bff13-3245-44f6-8a35-e0defef5898e',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
id: '3a8bff13-3245-44f6-8a35-e0defef5898e',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
test('renders a default toc renderer component', () => {
|
|
29
|
+
const store = mockStore({
|
|
30
|
+
intl: {
|
|
31
|
+
locale: 'en',
|
|
32
|
+
messages: {},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const component = renderer.create(
|
|
36
|
+
<Provider store={store}>
|
|
37
|
+
<MemoryRouter>
|
|
38
|
+
<DefaultTocRenderer data={data} tocEntries={tocEntries} />
|
|
39
|
+
</MemoryRouter>
|
|
40
|
+
</Provider>,
|
|
41
|
+
);
|
|
42
|
+
const json = component.toJSON();
|
|
43
|
+
expect(json).toMatchSnapshot();
|
|
44
|
+
});
|
|
@@ -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}
|