@plone/volto 17.0.0-alpha.19 → 17.0.0-alpha.20

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 CHANGED
@@ -8,6 +8,17 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 17.0.0-alpha.20 (2023-07-18)
12
+
13
+ ### Feature
14
+
15
+ - Use all the apiExpanders in use, so we perform a single request for getting all the required data. @sneridagh [#4946](https://github.com/plone/volto/issues/4946)
16
+
17
+ ### Bugfix
18
+
19
+ - Fix the condition deciding on listing pagination format so it takes into account container blocks as well @sneridagh [#4978](https://github.com/plone/volto/issues/4978)
20
+
21
+
11
22
  ## 17.0.0-alpha.19 (2023-07-18)
12
23
 
13
24
  ### Feature
@@ -688,10 +688,7 @@ Cypress.Commands.add(
688
688
  Cypress.Commands.add('toolbarSave', () => {
689
689
  // Save
690
690
  cy.get('#toolbar-save', { timeout: 10000 }).click();
691
- cy.waitForResourceToLoad('@navigation');
692
- cy.waitForResourceToLoad('@breadcrumbs');
693
- cy.waitForResourceToLoad('@actions');
694
- cy.waitForResourceToLoad('@types');
691
+ cy.waitForResourceToLoad('');
695
692
  });
696
693
 
697
694
  Cypress.Commands.add('clearSlate', (selector) => {
@@ -1,4 +1,5 @@
1
1
  export const slateBeforeEach = (contentType = 'Document') => {
2
+ cy.intercept('GET', `/**/*?expand*`).as('content');
2
3
  cy.autologin();
3
4
  cy.createContent({
4
5
  contentType: contentType,
@@ -6,12 +7,10 @@ export const slateBeforeEach = (contentType = 'Document') => {
6
7
  contentTitle: 'My Page',
7
8
  });
8
9
  cy.visit('/my-page');
9
- cy.waitForResourceToLoad('@navigation');
10
- cy.waitForResourceToLoad('@breadcrumbs');
11
- cy.waitForResourceToLoad('@actions');
12
- cy.waitForResourceToLoad('@types');
13
- cy.waitForResourceToLoad('my-page');
10
+ cy.wait('@content');
11
+
14
12
  cy.navigate('/my-page/edit');
13
+ cy.wait('@content');
15
14
  };
16
15
 
17
16
  export const getSelectedSlateEditor = () => {
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "17.0.0-alpha.19",
12
+ "version": "17.0.0-alpha.20",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "17.0.0-alpha.19",
3
+ "version": "17.0.0-alpha.20",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -148,6 +148,7 @@ export const widgetMapping = {
148
148
  title: TitleViewWidget,
149
149
  url: UrlViewWidget,
150
150
  internal_url: InternalUrlWidget,
151
+ object: () => '', // TODO: Not implemented yet: Object View widget
151
152
  },
152
153
  vocabulary: {},
153
154
  choices: SelectViewWidget,
@@ -77,6 +77,8 @@ let config = {
77
77
  okRoute: '/ok',
78
78
  apiPath,
79
79
  apiExpanders: [
80
+ // Added here for documentation purposes, addded at the end because it
81
+ // depends on a value of this object.
80
82
  // Add the following expanders for only issuing a single request.
81
83
  // https://6.docs.plone.org/volto/configuration/settings-reference.html#term-apiExpanders
82
84
  // {
@@ -185,6 +187,7 @@ let config = {
185
187
  querystringSearchGet: false,
186
188
  blockSettingsTabFieldsetsInitialStateOpen: true,
187
189
  excludeLinksAndReferencesMenuItem: false,
190
+ containerBlockTypes: ['gridBlock'],
188
191
  },
189
192
  experimental: {
190
193
  addBlockButton: {
@@ -215,6 +218,22 @@ let config = {
215
218
  components,
216
219
  };
217
220
 
221
+ // The apiExpanders depends on a config of the object, so it's done here
222
+ config.settings.apiExpanders = [
223
+ ...config.settings.apiExpanders,
224
+ {
225
+ match: '',
226
+ GET_CONTENT: ['breadcrumbs', 'actions', 'types'],
227
+ },
228
+ {
229
+ match: '',
230
+ GET_CONTENT: ['navigation'],
231
+ querystring: (config) => ({
232
+ 'expand.navigation.depth': config.settings.navDepth,
233
+ }),
234
+ },
235
+ ];
236
+
218
237
  ConfigRegistry.settings = config.settings;
219
238
  ConfigRegistry.experimental = config.experimental;
220
239
  ConfigRegistry.blocks = config.blocks;
@@ -550,3 +550,25 @@ export const getPreviousNextBlock = ({ content, block }) => {
550
550
 
551
551
  return [previousBlock, nextBlock];
552
552
  };
553
+
554
+ /**
555
+ * Given a `block` object and a list of block types, return a list of block ids matching the types
556
+ *
557
+ * @function findBlocks
558
+ * @param {Object} types A list with the list of types to be matched
559
+ * @return {Array} An array of block ids
560
+ */
561
+ export function findBlocks(blocks, types, result = []) {
562
+ const containerBlockTypes = config.settings.containerBlockTypes;
563
+
564
+ Object.keys(blocks).forEach((blockId) => {
565
+ const block = blocks[blockId];
566
+ if (types.includes(block['@type'])) {
567
+ result.push(blockId);
568
+ } else if (containerBlockTypes.includes(block['@type']) || block.blocks) {
569
+ findBlocks(block.blocks, types, result);
570
+ }
571
+ });
572
+
573
+ return result;
574
+ }
@@ -20,6 +20,7 @@ import {
20
20
  buildStyleClassNamesExtenders,
21
21
  getPreviousNextBlock,
22
22
  blocksFormGenerator,
23
+ findBlocks,
23
24
  } from './Blocks';
24
25
 
25
26
  import config from '@plone/volto/registry';
@@ -1284,7 +1285,7 @@ describe('Blocks', () => {
1284
1285
  blocks_layout: { items: [] },
1285
1286
  });
1286
1287
  });
1287
- it.only('Returns a filled blocks/blocks_layout pair with type block', () => {
1288
+ it('Returns a filled blocks/blocks_layout pair with type block', () => {
1288
1289
  const result = blocksFormGenerator(2, 'teaser');
1289
1290
  expect(Object.keys(result.blocks).length).toEqual(2);
1290
1291
  expect(result.blocks_layout.items.length).toEqual(2);
@@ -1297,3 +1298,60 @@ describe('Blocks', () => {
1297
1298
  });
1298
1299
  });
1299
1300
  });
1301
+
1302
+ describe('findBlocks', () => {
1303
+ it('Get all blocks in the first level (main block container)', () => {
1304
+ const blocks = {
1305
+ '1': { title: 'title', '@type': 'title' },
1306
+ '2': { title: 'an image', '@type': 'image' },
1307
+ '3': { title: 'description', '@type': 'description' },
1308
+ '4': { title: 'a text', '@type': 'slate' },
1309
+ };
1310
+ const types = ['description'];
1311
+ expect(findBlocks(blocks, types)).toStrictEqual(['3']);
1312
+ });
1313
+
1314
+ it('Get all blocks in the first level (main block container) given a list', () => {
1315
+ const blocks = {
1316
+ '1': { title: 'title', '@type': 'title' },
1317
+ '2': { title: 'an image', '@type': 'image' },
1318
+ '3': { title: 'description', '@type': 'description' },
1319
+ '4': { title: 'a text', '@type': 'slate' },
1320
+ };
1321
+ const types = ['description', 'slate'];
1322
+ expect(findBlocks(blocks, types)).toStrictEqual(['3', '4']);
1323
+ });
1324
+
1325
+ it('Get all blocks in the first level (main block container) given a list', () => {
1326
+ const blocks = {
1327
+ '1': { title: 'title', '@type': 'title' },
1328
+ '2': { title: 'an image', '@type': 'image' },
1329
+ '3': { title: 'description', '@type': 'description' },
1330
+ '4': { title: 'a text', '@type': 'slate' },
1331
+ '5': { title: 'a text', '@type': 'slate' },
1332
+ };
1333
+ const types = ['description', 'slate'];
1334
+ expect(findBlocks(blocks, types)).toStrictEqual(['3', '4', '5']);
1335
+ });
1336
+
1337
+ it('Get all blocks, including containers, given a list', () => {
1338
+ const blocks = {
1339
+ '1': { title: 'title', '@type': 'title' },
1340
+ '2': { title: 'an image', '@type': 'image' },
1341
+ '3': { title: 'description', '@type': 'description' },
1342
+ '4': { title: 'a text', '@type': 'slate' },
1343
+ '5': {
1344
+ title: 'a container',
1345
+ '@type': 'gridBlock',
1346
+ blocks: {
1347
+ '6': { title: 'title', '@type': 'title' },
1348
+ '7': { title: 'an image', '@type': 'image' },
1349
+ '8': { title: 'description', '@type': 'description' },
1350
+ '9': { title: 'a text', '@type': 'slate' },
1351
+ },
1352
+ },
1353
+ };
1354
+ const types = ['description', 'slate'];
1355
+ expect(findBlocks(blocks, types)).toStrictEqual(['3', '4', '8', '9']);
1356
+ });
1357
+ });
@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
2
2
  import { useHistory, useLocation } from 'react-router-dom';
3
3
  import qs from 'query-string';
4
4
  import { useSelector } from 'react-redux';
5
- import { slugify } from '@plone/volto/helpers/Utils/Utils';
5
+ import { findBlocks, slugify } from '@plone/volto/helpers';
6
6
 
7
7
  /**
8
8
  * @function useCreatePageQueryStringKey
@@ -12,13 +12,8 @@ import { slugify } from '@plone/volto/helpers/Utils/Utils';
12
12
  const useCreatePageQueryStringKey = (id) => {
13
13
  const blockTypesWithPagination = ['search', 'listing'];
14
14
  const blocks = useSelector((state) => state?.content?.data?.blocks) || [];
15
- const blocksLayout =
16
- useSelector((state) => state?.content?.data?.blocks_layout?.items) || [];
17
- const displayedBlocks = blocksLayout?.map((item) => blocks[item]);
18
15
  const hasMultiplePaginations =
19
- displayedBlocks.filter((item) =>
20
- blockTypesWithPagination.includes(item['@type']),
21
- ).length > 1 || false;
16
+ findBlocks(blocks, blockTypesWithPagination).length > 1;
22
17
 
23
18
  return hasMultiplePaginations ? slugify(`page-${id}`) : 'page';
24
19
  };
@@ -58,6 +58,7 @@ export {
58
58
  buildStyleClassNamesFromData,
59
59
  buildStyleClassNamesExtenders,
60
60
  getPreviousNextBlock,
61
+ findBlocks,
61
62
  } from '@plone/volto/helpers/Blocks/Blocks';
62
63
  export { default as BodyClass } from '@plone/volto/helpers/BodyClass/BodyClass';
63
64
  export { default as ScrollToTop } from '@plone/volto/helpers/ScrollToTop/ScrollToTop';
@@ -93,6 +94,7 @@ export {
93
94
  arrayRange,
94
95
  reorderArray,
95
96
  isInteractiveElement,
97
+ slugify,
96
98
  } from '@plone/volto/helpers/Utils/Utils';
97
99
  export { messages } from './MessageLabels/MessageLabels';
98
100
  export {
@@ -67,7 +67,15 @@ export function addExpandersToPath(path, type, isAnonymous) {
67
67
 
68
68
  const querystringFromConfig = apiExpanders
69
69
  .filter((expand) => matchPath(url, expand.match) && expand[type])
70
- .reduce((acc, expand) => ({ ...acc, ...expand?.['querystring'] }), {});
70
+ .reduce((acc, expand) => {
71
+ let querystring = expand?.['querystring'];
72
+ // The querystring accepts being a function to be able to take other
73
+ // config parameters
74
+ if (typeof querystring === 'function') {
75
+ querystring = querystring(config);
76
+ }
77
+ return { ...acc, ...querystring };
78
+ }, {});
71
79
 
72
80
  const queryMerge = { ...query, ...querystringFromConfig };
73
81
 
@@ -122,7 +130,13 @@ const apiMiddlewareFactory = (api) => ({ dispatch, getState }) => (next) => (
122
130
  ) => {
123
131
  const { settings } = config;
124
132
 
125
- const isAnonymous = !getState().userSession.token;
133
+ const token = getState().userSession.token;
134
+ let isAnonymous = true;
135
+ if (token) {
136
+ const tokenExpiration = jwtDecode(token).exp;
137
+ const currentTime = new Date().getTime() / 1000;
138
+ isAnonymous = !token || currentTime > tokenExpiration;
139
+ }
126
140
 
127
141
  if (typeof action === 'function') {
128
142
  return action(dispatch, getState);
@@ -57,11 +57,13 @@ export default function actions(state = initialState, action = {}) {
57
57
  }
58
58
  return state;
59
59
  case `${LIST_ACTIONS}_SUCCESS`:
60
- hasExpander = hasApiExpander(
61
- 'actions',
62
- getBaseUrl(flattenToAppURL(action.result['@id'])),
63
- );
64
- if (!hasExpander) {
60
+ // Even if the expander is set or not, if the LIST_ACTIONS is
61
+ // called, we want it to store the data if the actions data is
62
+ // not set in the expander data (['@components']) but in the "normal"
63
+ // action result (we look for the object property returned by the endpoint)
64
+ // Unfortunately, this endpoint returns all the actions in a plain object
65
+ // with no structure :(
66
+ if (action.result.object) {
65
67
  return {
66
68
  ...state,
67
69
  error: null,
@@ -207,4 +207,74 @@ describe('Actions reducer - (ACTIONS)GET_CONTENT', () => {
207
207
  loading: false,
208
208
  });
209
209
  });
210
+
211
+ it('should handle (ACTIONS)LIST_ACTIONS (standalone with apiExpander enabled)', () => {
212
+ expect(
213
+ actions(undefined, {
214
+ type: `${LIST_ACTIONS}_SUCCESS`,
215
+ result: {
216
+ object: [],
217
+ object_buttons: [],
218
+ site_actions: [],
219
+ user: [
220
+ {
221
+ icon: '',
222
+ id: 'preferences',
223
+ title: 'Preferences',
224
+ },
225
+ {
226
+ icon: '',
227
+ id: 'dashboard',
228
+ title: 'Dashboard',
229
+ },
230
+ {
231
+ icon: '',
232
+ id: 'plone_setup',
233
+ title: 'Site Setup',
234
+ },
235
+ {
236
+ icon: '',
237
+ id: 'logout',
238
+ title: 'Log out',
239
+ },
240
+ ],
241
+ document_actions: [],
242
+ portal_tabs: [],
243
+ },
244
+ }),
245
+ ).toEqual({
246
+ error: null,
247
+ actions: {
248
+ object: [],
249
+ object_buttons: [],
250
+ site_actions: [],
251
+ user: [
252
+ {
253
+ icon: '',
254
+ id: 'preferences',
255
+ title: 'Preferences',
256
+ },
257
+ {
258
+ icon: '',
259
+ id: 'dashboard',
260
+ title: 'Dashboard',
261
+ },
262
+ {
263
+ icon: '',
264
+ id: 'plone_setup',
265
+ title: 'Site Setup',
266
+ },
267
+ {
268
+ icon: '',
269
+ id: 'logout',
270
+ title: 'Log out',
271
+ },
272
+ ],
273
+ document_actions: [],
274
+ portal_tabs: [],
275
+ },
276
+ loaded: true,
277
+ loading: false,
278
+ });
279
+ });
210
280
  });
@@ -83,6 +83,7 @@ config.set('settings', {
83
83
  styleClassNameConverters,
84
84
  styleClassNameExtenders,
85
85
  blockSettingsTabFieldsetsInitialStateOpen: true,
86
+ containerBlockTypes: [],
86
87
  });
87
88
  config.set('blocks', {
88
89
  blocksConfig: {