@plone/volto 19.1.4 → 19.1.6

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/locales/af/LC_MESSAGES/volto.po +18 -16
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +18 -16
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +18 -16
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +18 -16
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +18 -16
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +18 -16
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +18 -16
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +18 -16
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +18 -16
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +18 -16
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +18 -16
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +18 -16
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +18 -16
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +18 -16
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +18 -16
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +18 -16
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +18 -16
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +18 -16
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +18 -16
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +18 -16
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +18 -16
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +18 -16
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +18 -16
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +18 -16
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +18 -16
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +18 -16
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +18 -16
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +18 -16
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +18 -16
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +18 -16
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +18 -16
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +18 -16
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +18 -16
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +18 -16
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +18 -16
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +18 -16
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +18 -16
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +18 -16
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +18 -16
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +18 -16
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +18 -16
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +18 -16
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +18 -16
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +18 -16
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +18 -16
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +18 -16
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +18 -16
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +18 -16
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +18 -16
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +18 -16
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +18 -16
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +18 -16
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +18 -16
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +18 -16
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +18 -16
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +18 -16
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +18 -16
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +18 -16
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +18 -16
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +18 -16
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +18 -16
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +18 -16
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +17 -15
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +18 -16
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +18 -16
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +18 -16
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +5 -6
  134. package/src/actions/querystringsearch/querystringsearch.js +4 -1
  135. package/src/actions/querystringsearch/querystringsearch.test.js +77 -0
  136. package/src/components/manage/Controlpanels/BlockTypes.test.ts +132 -0
  137. package/src/components/manage/Controlpanels/BlockTypes.tsx +32 -11
  138. package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +23 -2
  139. package/src/components/manage/Toolbar/More.jsx +13 -0
  140. package/src/components/manage/Widgets/ArrayWidget.jsx +3 -2
  141. package/src/components/manage/Widgets/FileWidget.jsx +19 -9
  142. package/src/components/manage/Widgets/QuerystringWidget.jsx +1 -18
  143. package/src/components/manage/Widgets/QuerystringWidget.test.jsx +42 -1
  144. package/src/components/manage/Widgets/RegistryImageWidget.jsx +5 -3
  145. package/src/components/manage/Widgets/SelectStyling.jsx +33 -1
  146. package/src/components/manage/Widgets/SelectWidget.jsx +3 -2
  147. package/theme/themes/pastanaga/extras/focus.less +33 -0
  148. package/theme/themes/pastanaga/extras/main.less +1 -0
  149. package/theme/themes/pastanaga/extras/skiplinks.less +0 -2
  150. package/theme/themes/pastanaga/globals/site.variables +7 -0
  151. package/types/components/manage/Controlpanels/BlockTypes.d.ts +48 -0
  152. package/types/components/manage/Widgets/QuerystringWidget.d.ts +0 -4
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "19.1.4",
12
+ "version": "19.1.6",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -113,7 +113,6 @@
113
113
  "is-url": "1.2.4",
114
114
  "jotai": "2.11.3",
115
115
  "jwt-decode": "2.2.0",
116
- "linkify-it": "3.0.2",
117
116
  "locale": "0.1.0",
118
117
  "lodash": "4.18.1",
119
118
  "lodash-move": "1.1.1",
@@ -178,9 +177,9 @@
178
177
  "use-deep-compare-effect": "1.8.1",
179
178
  "uuid": "^14.0.0",
180
179
  "@plone/registry": "3.0.1",
181
- "@plone/components": "4.2.0",
182
- "@plone/volto-slate": "19.0.2",
183
- "@plone/scripts": "4.0.1"
180
+ "@plone/components": "4.2.1",
181
+ "@plone/scripts": "4.0.1",
182
+ "@plone/volto-slate": "19.0.4"
184
183
  },
185
184
  "devDependencies": {
186
185
  "@babel/core": "^7.28.5",
@@ -290,8 +289,8 @@
290
289
  "webpack-node-externals": "3.0.0",
291
290
  "@plone/babel-preset-razzle": "^1.0.1",
292
291
  "@plone/razzle": "1.0.0",
293
- "@plone/razzle-dev-utils": "1.0.0",
294
292
  "@plone/types": "2.0.0",
293
+ "@plone/razzle-dev-utils": "1.0.0",
295
294
  "@plone/volto-coresandbox": "1.0.0"
296
295
  },
297
296
  "scripts": {
@@ -17,7 +17,10 @@ export function getQueryStringResults(path, data, subrequest, page) {
17
17
  delete requestData.depth;
18
18
  requestData.query.forEach((q) => {
19
19
  if (q.i === 'path') {
20
- q.v += '::' + data.depth;
20
+ // fixes https://github.com/plone/volto/issues/8349
21
+ // Skip if depth is already embedded in the path value (e.g. set via object browser)
22
+ const hasEmbeddedDepth = /::\d+$/.test(q.v);
23
+ if (!hasEmbeddedDepth) q.v += '::' + data.depth;
21
24
  }
22
25
  });
23
26
  }
@@ -73,4 +73,81 @@ describe('querystringsearch action', () => {
73
73
  expect(action.request.path).toEqual('/@querystring-search');
74
74
  expect(action.request.data.sort_order).toEqual('descending');
75
75
  });
76
+
77
+ describe('depth handling', () => {
78
+ it('appends depth to path value when depth is set and path has no embedded depth', () => {
79
+ const data = {
80
+ query: [
81
+ {
82
+ i: 'path',
83
+ o: 'plone.app.querystring.operation.string.path',
84
+ v: '/folder',
85
+ },
86
+ ],
87
+ depth: 2,
88
+ };
89
+ const action = getQueryStringResults('', data);
90
+
91
+ const pathQuery = action.request.data.query.find((q) => q.i === 'path');
92
+ expect(pathQuery.v).toEqual('/folder::2');
93
+ });
94
+
95
+ it('does not double-append depth when path value already contains an embedded depth', () => {
96
+ const data = {
97
+ query: [
98
+ {
99
+ i: 'path',
100
+ o: 'plone.app.querystring.operation.string.path',
101
+ v: '/folder::1',
102
+ },
103
+ ],
104
+ depth: 3,
105
+ };
106
+ const action = getQueryStringResults('', data);
107
+
108
+ const pathQuery = action.request.data.query.find((q) => q.i === 'path');
109
+ expect(pathQuery.v).toEqual('/folder::1');
110
+ });
111
+
112
+ it('removes depth from the top-level request data', () => {
113
+ const data = {
114
+ query: [
115
+ {
116
+ i: 'path',
117
+ o: 'plone.app.querystring.operation.string.path',
118
+ v: '/folder',
119
+ },
120
+ ],
121
+ depth: 2,
122
+ };
123
+ const action = getQueryStringResults('', data);
124
+
125
+ expect(action.request.data.depth).toBeUndefined();
126
+ });
127
+
128
+ it('handles mixed path entries: appends depth only to those without embedded depth', () => {
129
+ const data = {
130
+ query: [
131
+ {
132
+ i: 'path',
133
+ o: 'plone.app.querystring.operation.string.path',
134
+ v: '/folder-a',
135
+ },
136
+ {
137
+ i: 'path',
138
+ o: 'plone.app.querystring.operation.string.path',
139
+ v: '/folder-b::5',
140
+ },
141
+ ],
142
+ depth: 2,
143
+ };
144
+ const action = getQueryStringResults('', data);
145
+
146
+ const [pathA, pathB] = action.request.data.query.filter(
147
+ (q) => q.i === 'path',
148
+ );
149
+ expect(pathA.v).toEqual('/folder-a::2');
150
+ expect(pathB.v).toEqual('/folder-b::5');
151
+ });
152
+ });
76
153
  });
@@ -0,0 +1,132 @@
1
+ import { createIntl, createIntlCache } from 'react-intl';
2
+ import type { BlocksConfigData } from '@plone/types';
3
+ import { sortConfigBlocks } from './BlockTypes';
4
+
5
+ const intl = createIntl(
6
+ { locale: 'en', messages: { Teaser: 'Destaque' } },
7
+ createIntlCache(),
8
+ );
9
+
10
+ // `BlockConfigBase` types id and title as required, so a malformed entry
11
+ // cannot be expressed through it. That mismatch between the type and the
12
+ // configuration is the bug these tests cover, so the fixtures go around it.
13
+ const asBlocksConfig = (config: Record<string, unknown>) =>
14
+ config as unknown as BlocksConfigData;
15
+
16
+ describe('sortConfigBlocks', () => {
17
+ it('lists a registered block', () => {
18
+ const blocks = sortConfigBlocks(
19
+ asBlocksConfig({ image: { id: 'image', title: 'Image' } }),
20
+ intl,
21
+ );
22
+
23
+ expect(blocks).toEqual([{ id: 'image', title: 'Image' }]);
24
+ });
25
+
26
+ it('translates the title', () => {
27
+ const blocks = sortConfigBlocks(
28
+ asBlocksConfig({ teaser: { id: 'teaser', title: 'Teaser' } }),
29
+ intl,
30
+ );
31
+
32
+ expect(blocks[0].title).toEqual('Destaque');
33
+ });
34
+
35
+ it('sorts by the translated title, not by the config order', () => {
36
+ const blocks = sortConfigBlocks(
37
+ asBlocksConfig({
38
+ zebra: { id: 'zebra', title: 'Zebra' },
39
+ apple: { id: 'apple', title: 'Apple' },
40
+ // 'Teaser' translates to 'Destaque', so it sorts before 'Zebra'.
41
+ teaser: { id: 'teaser', title: 'Teaser' },
42
+ }),
43
+ intl,
44
+ );
45
+
46
+ expect(blocks.map((block) => block.title)).toEqual([
47
+ 'Apple',
48
+ 'Destaque',
49
+ 'Zebra',
50
+ ]);
51
+ });
52
+
53
+ it('keeps the rest of the block config', () => {
54
+ const blocks = sortConfigBlocks(
55
+ asBlocksConfig({
56
+ image: {
57
+ id: 'image',
58
+ title: 'Image',
59
+ icon: 'image.svg',
60
+ group: 'media',
61
+ },
62
+ }),
63
+ intl,
64
+ );
65
+
66
+ expect(blocks[0]).toMatchObject({ icon: 'image.svg', group: 'media' });
67
+ });
68
+
69
+ describe('entries that are not blocks', () => {
70
+ // An add-on that configures a block which was never registered leaves an
71
+ // entry behind carrying only what it set. Listing it would link to a block
72
+ // that does not exist, and a row with no id crashes the table.
73
+ it('drops an entry with neither an id nor a title', () => {
74
+ const blocks = sortConfigBlocks(
75
+ asBlocksConfig({
76
+ image: { id: 'image', title: 'Image' },
77
+ accordion: { addPermission: [] },
78
+ }),
79
+ intl,
80
+ );
81
+
82
+ expect(blocks).toEqual([{ id: 'image', title: 'Image' }]);
83
+ });
84
+
85
+ it('drops an entry with an id but no title', () => {
86
+ const blocks = sortConfigBlocks(
87
+ asBlocksConfig({ accordion: { id: 'accordion' } }),
88
+ intl,
89
+ );
90
+
91
+ expect(blocks).toEqual([]);
92
+ });
93
+
94
+ it('drops an entry with a title but no id', () => {
95
+ const blocks = sortConfigBlocks(
96
+ asBlocksConfig({ accordion: { title: 'Accordion' } }),
97
+ intl,
98
+ );
99
+
100
+ expect(blocks).toEqual([]);
101
+ });
102
+
103
+ it('does not fall back to the config key for a missing id', () => {
104
+ const blocks = sortConfigBlocks(
105
+ asBlocksConfig({ accordion: { title: 'Accordion' } }),
106
+ intl,
107
+ );
108
+
109
+ expect(blocks.map((block) => block.id)).not.toContain('accordion');
110
+ });
111
+
112
+ it('gives every listed block an id, so the table can key its rows', () => {
113
+ const blocks = sortConfigBlocks(
114
+ asBlocksConfig({
115
+ image: { id: 'image', title: 'Image' },
116
+ accordion: { addPermission: [] },
117
+ teaser: { id: 'teaser' },
118
+ }),
119
+ intl,
120
+ );
121
+
122
+ expect(blocks.length).toBeGreaterThan(0);
123
+ for (const block of blocks) {
124
+ expect(block.id).toBeTruthy();
125
+ }
126
+ });
127
+ });
128
+
129
+ it('returns nothing when no block is configured', () => {
130
+ expect(sortConfigBlocks(asBlocksConfig({}), intl)).toEqual([]);
131
+ });
132
+ });
@@ -5,6 +5,7 @@ import { useClient } from '@plone/volto/hooks';
5
5
  import config from '@plone/volto/registry';
6
6
  import { createPortal } from 'react-dom';
7
7
  import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
8
+ import type { IntlShape } from 'react-intl';
8
9
  import { Link } from 'react-router-dom';
9
10
  import { useEffect } from 'react';
10
11
  import { getBlockTypes } from '@plone/volto/actions/blockTypes/blockTypes';
@@ -15,6 +16,7 @@ import UniversalLink from '@plone/volto/components/manage/UniversalLink/Universa
15
16
 
16
17
  import backSVG from '@plone/volto/icons/back.svg';
17
18
  import type { Location } from 'history';
19
+ import type { BlocksConfigData } from '@plone/types';
18
20
 
19
21
  const messages = defineMessages({
20
22
  back: {
@@ -53,6 +55,35 @@ type SelectorState = {
53
55
  blockTypes: BlockTypesState;
54
56
  };
55
57
 
58
+ /**
59
+ * List the configured blocks, by title.
60
+ *
61
+ * An add-on can configure a block that was never registered — enhancing one
62
+ * that ships with another add-on, say — which leaves an entry behind that is
63
+ * not a block at all, with no id or no title. `BlockConfigBase` says both are
64
+ * always there, but the config disagrees, so drop those entries rather than
65
+ * list a block that does not exist.
66
+ * @function sortConfigBlocks
67
+ * @param {BlocksConfigData} blocksConfig The blocks configuration.
68
+ * @param {IntlShape} intl Used to translate each block title.
69
+ * @returns {Array} The configured blocks, sorted by translated title.
70
+ */
71
+ export function sortConfigBlocks(
72
+ blocksConfig: BlocksConfigData,
73
+ intl: IntlShape,
74
+ ) {
75
+ return Object.entries(blocksConfig)
76
+ .filter(([, blockConfig]) => blockConfig.id && blockConfig.title)
77
+ .map(([, blockConfig]) => ({
78
+ ...blockConfig,
79
+ title: intl.formatMessage({
80
+ id: blockConfig.title,
81
+ defaultMessage: blockConfig.title,
82
+ }),
83
+ }))
84
+ .sort((a, b) => (a.title === b.title ? 0 : a.title > b.title ? 1 : -1));
85
+ }
86
+
56
87
  const BlockTypesControlpanel = (props: RouteProps) => {
57
88
  const { location } = props;
58
89
  const intl = useIntl();
@@ -62,17 +93,7 @@ const BlockTypesControlpanel = (props: RouteProps) => {
62
93
  const pathname = location.pathname;
63
94
  const isClient = useClient();
64
95
 
65
- const blocks = Object.entries(blocksConfig)
66
- .map(([key, blockConfig]) => ({
67
- ...blockConfig,
68
- title: blockConfig.title
69
- ? intl.formatMessage({
70
- id: blockConfig.title,
71
- defaultMessage: blockConfig.title,
72
- })
73
- : blockConfig.id ?? key,
74
- }))
75
- .sort((a, b) => (a.title === b.title ? 0 : a.title > b.title ? 1 : -1));
96
+ const blocks = sortConfigBlocks(blocksConfig, intl);
76
97
 
77
98
  useEffect(() => {
78
99
  dispatch(getBlockTypes());
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
5
5
  import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
6
6
  import { Input, Segment, Breadcrumb } from 'semantic-ui-react';
7
7
 
8
+ import debounce from 'lodash/debounce';
8
9
  import join from 'lodash/join';
9
10
 
10
11
  // These absolute imports (without using the corresponding centralized index.js) are required
@@ -159,6 +160,22 @@ class ObjectBrowserBody extends Component {
159
160
  view: this.props.mode === 'image' ? 'icons' : 'list',
160
161
  };
161
162
  this.searchInputRef = React.createRef();
163
+ // Debounce the live-search dispatch so rapid keystrokes do not produce a
164
+ // burst of overlapping requests whose responses may resolve out of order
165
+ // (an older, less-filtered response arriving after the latest one would
166
+ // visually "reset" the list to look unfiltered).
167
+ this.debouncedSearch = debounce(this.doSearch, 300);
168
+ }
169
+
170
+ /**
171
+ * Component will unmount
172
+ * @method componentWillUnmount
173
+ * @returns {undefined}
174
+ */
175
+ componentWillUnmount() {
176
+ if (this.debouncedSearch) {
177
+ this.debouncedSearch.cancel();
178
+ }
162
179
  }
163
180
 
164
181
  /**
@@ -248,8 +265,7 @@ class ObjectBrowserBody extends Component {
248
265
  view: prevState.view === 'icons' ? 'list' : 'icons',
249
266
  }));
250
267
 
251
- onSearch = (e) => {
252
- const text = flattenToAppURL(e.target.value);
268
+ doSearch = (text) => {
253
269
  if (text.startsWith('/')) {
254
270
  this.setState({ currentFolder: text });
255
271
  this.props.searchContent(
@@ -286,6 +302,11 @@ class ObjectBrowserBody extends Component {
286
302
  }
287
303
  };
288
304
 
305
+ onSearch = (e) => {
306
+ const text = flattenToAppURL(e.target.value);
307
+ this.debouncedSearch(text);
308
+ };
309
+
289
310
  onSelectItem = (item) => {
290
311
  const url = item['@id'];
291
312
  const { block, data, mode, dataName, onChangeBlock } = this.props;
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
4
4
  import { useDispatch, useSelector, shallowEqual } from 'react-redux';
5
5
  import { Link, useHistory } from 'react-router-dom';
6
6
  import find from 'lodash/find';
7
+ import { Dimmer, Loader } from 'semantic-ui-react';
7
8
  import { toast } from 'react-toastify';
8
9
 
9
10
  import Toast from '@plone/volto/components/manage/Toast/Toast';
@@ -103,6 +104,10 @@ const messages = defineMessages({
103
104
  id: 'workingCopyGenericError',
104
105
  defaultMessage: 'An error occurred while performing this operation.',
105
106
  },
107
+ Loading: {
108
+ id: 'Loading',
109
+ defaultMessage: 'Loading',
110
+ },
106
111
  });
107
112
 
108
113
  const More = (props) => {
@@ -123,6 +128,8 @@ const More = (props) => {
123
128
  const workingCopyApply = workingCopy?.apply.loading;
124
129
  const workingCopyCreate = workingCopy?.create.loading;
125
130
  const workingCopyRemove = workingCopy?.remove.loading;
131
+ const isWorkingCopyLoading =
132
+ workingCopyApply || workingCopyCreate || workingCopyRemove;
126
133
 
127
134
  const prevWorkingCopyApplyLoading = usePrevious(workingCopyApply);
128
135
  const prevWorkingCopyCreateLoading = usePrevious(workingCopyCreate);
@@ -216,6 +223,9 @@ const More = (props) => {
216
223
  : null,
217
224
  }}
218
225
  >
226
+ <Dimmer active={isWorkingCopyLoading} inverted>
227
+ <Loader>{intl.formatMessage(messages.Loading)}</Loader>
228
+ </Dimmer>
219
229
  <header>
220
230
  <h2>{content.title}</h2>
221
231
  <button
@@ -335,6 +345,7 @@ const More = (props) => {
335
345
  <li>
336
346
  <button
337
347
  aria-label={intl.formatMessage(messages.CreateWorkingCopy)}
348
+ disabled={workingCopyCreate}
338
349
  onClick={() => {
339
350
  dispatch(createWorkingCopy(path)).then((response) => {
340
351
  history.push(flattenToAppURL(response['@id']));
@@ -354,6 +365,7 @@ const More = (props) => {
354
365
  <li>
355
366
  <button
356
367
  aria-label={intl.formatMessage(messages.applyWorkingCopy)}
368
+ disabled={workingCopyApply}
357
369
  onClick={() => {
358
370
  dispatch(applyWorkingCopy(path)).then((response) => {
359
371
  history.push(flattenToAppURL(content.working_copy_of['@id']));
@@ -395,6 +407,7 @@ const More = (props) => {
395
407
  <li>
396
408
  <button
397
409
  aria-label={intl.formatMessage(messages.removeWorkingCopy)}
410
+ disabled={workingCopyRemove}
398
411
  onClick={() => {
399
412
  dispatch(removeWorkingCopy(path)).then((response) => {
400
413
  history.push(flattenToAppURL(content.working_copy_of['@id']));
@@ -328,7 +328,8 @@ class ArrayWidget extends Component {
328
328
  <SelectComponent
329
329
  menuShouldScrollIntoView={false}
330
330
  id={`field-${this.props.id}`}
331
- aria-labelledby={`fieldset-${this.props.fieldSet}-field-label-${this.props.id}`}
331
+ fieldTitle={this.props.title}
332
+ aria-label={this.props.title || undefined}
332
333
  key={this.props.id}
333
334
  isDisabled={this.props.disabled || this.props.isDisabled}
334
335
  className="react-select-container"
@@ -400,7 +401,7 @@ class ArrayWidget extends Component {
400
401
  )
401
402
  )
402
403
  }
403
- isClearable
404
+ isClearable={!this.props.required}
404
405
  isMulti
405
406
  />
406
407
  </SortableContext>
@@ -35,12 +35,14 @@ const messages = defineMessages({
35
35
  defaultMessage: 'Drop files here ...',
36
36
  },
37
37
  editFile: {
38
- id: 'Drop a file here or click to replace the existing file',
39
- defaultMessage: 'Drop a file here or click to replace the existing file',
38
+ id: 'Drop file here to replace the existing file',
39
+ defaultMessage:
40
+ 'File upload area. Press Enter or click to replace the existing file.',
40
41
  },
41
42
  fileDrag: {
42
- id: 'Drop a file here or click to upload',
43
- defaultMessage: 'Drop a file here or click to upload',
43
+ id: 'Drop file here to upload a new file',
44
+ defaultMessage:
45
+ 'File upload area. Press Enter or click to open the file browser.',
44
46
  },
45
47
  replaceFile: {
46
48
  id: 'Replace existing file',
@@ -60,8 +62,14 @@ const messages = defineMessages({
60
62
  defaultMessage: 'File is not of the accepted type {accept}',
61
63
  },
62
64
  dragAndDropActionA11y: {
63
- id: 'File upload area. Press Enter to open the file browser',
64
- defaultMessage: 'File upload area. Press Enter to open the file browser',
65
+ id: 'Press Enter to browse files from your computer.',
66
+ defaultMessage:
67
+ 'File upload area. Press Enter or click to open the file browser.',
68
+ },
69
+ dragAndDropReplaceA11y: {
70
+ id: 'File upload area. Press Enter or click to replace the existing file',
71
+ defaultMessage:
72
+ 'File upload area. Press Enter or click to replace the existing file.',
65
73
  },
66
74
  requiredField: {
67
75
  id: 'This field is required.',
@@ -180,9 +188,11 @@ const FileWidget = (props) => {
180
188
  };
181
189
 
182
190
  const statusTextA11y = [
183
- intl.formatMessage(messages.dragAndDropActionA11y), // Interaction instructions
184
- props.required && intl.formatMessage(messages.requiredField), // Required field status
185
- value?.filename, // Current file name if a file is uploaded
191
+ value
192
+ ? intl.formatMessage(messages.dragAndDropReplaceA11y)
193
+ : intl.formatMessage(messages.dragAndDropActionA11y),
194
+ props.required && intl.formatMessage(messages.requiredField),
195
+ value?.filename,
186
196
  ]
187
197
  .filter(Boolean)
188
198
  .join('. ');
@@ -7,10 +7,6 @@ const messages = defineMessages({
7
7
  id: 'Criteria',
8
8
  defaultMessage: 'Criteria',
9
9
  },
10
- depth: {
11
- id: 'Depth',
12
- defaultMessage: 'Depth',
13
- },
14
10
  SortOn: {
15
11
  id: 'Sort on',
16
12
  defaultMessage: 'Sort on',
@@ -38,16 +34,7 @@ export const objectSchema = ({ intl, isDisabled, value }) => ({
38
34
  {
39
35
  id: 'default',
40
36
  title: 'Default',
41
- fields: [
42
- 'query',
43
- ...(value?.query?.filter((q) => q.i === 'path').length > 0
44
- ? ['depth']
45
- : []),
46
- 'sort_on',
47
- 'sort_order_boolean',
48
- 'limit',
49
- 'b_size',
50
- ],
37
+ fields: ['query', 'sort_on', 'sort_order_boolean', 'limit', 'b_size'],
51
38
  },
52
39
  ],
53
40
  properties: {
@@ -55,10 +42,6 @@ export const objectSchema = ({ intl, isDisabled, value }) => ({
55
42
  title: intl.formatMessage(messages.Criteria),
56
43
  widget: 'query',
57
44
  },
58
- depth: {
59
- title: intl.formatMessage(messages.depth),
60
- type: 'number',
61
- },
62
45
  sort_on: {
63
46
  title: intl.formatMessage(messages.SortOn),
64
47
  widget: 'query_sort_on',
@@ -4,7 +4,7 @@ import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
5
  import { waitFor } from '@testing-library/react';
6
6
 
7
- import QuerystringWidget from './QuerystringWidget';
7
+ import QuerystringWidget, { objectSchema } from './QuerystringWidget';
8
8
 
9
9
  const mockStore = configureStore();
10
10
 
@@ -25,6 +25,47 @@ test('renders an querystring widget component', async () => {
25
25
  expect(component.toJSON()).toMatchSnapshot();
26
26
  });
27
27
 
28
+ describe('objectSchema', () => {
29
+ const mockIntl = { formatMessage: ({ defaultMessage }) => defaultMessage };
30
+
31
+ it('does not include depth in the default fieldset', () => {
32
+ const schema = objectSchema({ intl: mockIntl });
33
+ const fields = schema.fieldsets[0].fields;
34
+
35
+ expect(fields).not.toContain('depth');
36
+ expect(fields).toEqual([
37
+ 'query',
38
+ 'sort_on',
39
+ 'sort_order_boolean',
40
+ 'limit',
41
+ 'b_size',
42
+ ]);
43
+ });
44
+
45
+ it('does not include depth in the schema properties', () => {
46
+ const schema = objectSchema({ intl: mockIntl });
47
+
48
+ expect(schema.properties).not.toHaveProperty('depth');
49
+ });
50
+
51
+ it('does not include depth even when value contains a path criterion', () => {
52
+ const value = {
53
+ query: [
54
+ {
55
+ i: 'path',
56
+ o: 'plone.app.querystring.operation.string.path',
57
+ v: '/folder',
58
+ },
59
+ ],
60
+ };
61
+ const schema = objectSchema({ intl: mockIntl, value });
62
+ const fields = schema.fieldsets[0].fields;
63
+
64
+ expect(fields).not.toContain('depth');
65
+ expect(schema.properties).not.toHaveProperty('depth');
66
+ });
67
+ });
68
+
28
69
  test('can take a schemaEnhancer', async () => {
29
70
  const store = mockStore({
30
71
  querystring: { indexes: {} },
@@ -33,12 +33,14 @@ const messages = defineMessages({
33
33
  defaultMessage: 'Drop files here ...',
34
34
  },
35
35
  editFile: {
36
- id: 'Drop a file here or click to replace the existing file',
37
- defaultMessage: 'Drop a file here or click to replace the existing file',
36
+ id: 'Drop file here to replace the existing file',
37
+ defaultMessage:
38
+ 'File upload area. Press Enter or click to replace the existing file.',
38
39
  },
39
40
  fileDrag: {
40
41
  id: 'Drop file here to upload a new file',
41
- defaultMessage: 'Drop file here to upload a new file',
42
+ defaultMessage:
43
+ 'File upload area. Press Enter or click to open the file browser.',
42
44
  },
43
45
  replaceFile: {
44
46
  id: 'Replace existing file',