@kitconcept/volto-light-theme 7.0.0-alpha.15 → 7.0.0-alpha.16
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 +9 -15
- package/.release-it-local.json +31 -0
- package/CHANGELOG.md +16 -0
- package/package.json +2 -2
- package/src/components/Blocks/EventCalendar/Search/SearchBlockEdit.jsx +2 -2
- package/src/components/Blocks/EventCalendar/Search/components/DateRangePicker.tsx +14 -1
- package/src/components/Blocks/EventCalendar/Search/components/EventTemplate.tsx +11 -5
- package/src/components/Blocks/EventCalendar/Search/hocs/withSearch.jsx +26 -4
- package/src/components/Blocks/EventCalendar/Search/layout/TopSideFacets.jsx +15 -3
- package/src/components/Blocks/EventCalendar/Search/schema.js +8 -0
- package/src/components/Blocks/Teaser/DefaultBody.tsx +1 -0
- package/src/components/Theming/ConfigInjector.tsx +32 -0
- package/src/components/Widgets/BlockConfigJSONEditor.tsx +58 -0
- package/src/config/settings.ts +1 -1
- package/src/config/slots.ts +6 -0
- package/src/config/widgets.ts +4 -0
- package/src/helpers/BlocksConfigMerger.test.ts +97 -0
- package/src/helpers/BlocksConfigMerger.ts +42 -0
- package/src/index.ts +2 -0
- package/src/theme/_layout.scss +2 -1
- package/src/theme/_widgets.scss +60 -0
- package/src/theme/blocks/_eventSearch.scss +22 -3
- package/src/types.d.ts +14 -1
- package/vitest.config.mjs +7 -4
package/.changelog.draft
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
## 7.0.0-alpha.
|
|
2
|
-
|
|
3
|
-
### Breaking
|
|
4
|
-
|
|
5
|
-
- We renamed this three fields in the `kitconcept.footer` behavior. @sneridagh
|
|
6
|
-
`footer_main_logo_inversed` => `footer_logo`
|
|
7
|
-
`footer_logo` => `post_footer_logo`
|
|
8
|
-
`footer_logo_link` => `post_footer_logo_link`
|
|
1
|
+
## 7.0.0-alpha.16 (2025-07-23)
|
|
9
2
|
|
|
10
3
|
### Feature
|
|
11
4
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
5
|
+
- Add query support in eventCalendar Block. @iFlameing [#609](https://github.com/kitconcept/volto-light-theme/pull/609)
|
|
6
|
+
- Added support for the blocks configuration TTW behavior. @sneridagh [#614](https://github.com/kitconcept/volto-light-theme/pull/614)
|
|
7
|
+
- Update carousel block version, example content and cypress @iRohitSingh [#616](https://github.com/kitconcept/volto-light-theme/pull/616)
|
|
14
8
|
|
|
15
9
|
### Bugfix
|
|
16
10
|
|
|
17
|
-
- Fixed
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
11
|
+
- Fixed missing hide_description prop in the Summary component within
|
|
12
|
+
TeaserDefaultBodyTemplate and add cypress test for carousel block @iRohitSingh [#610](https://github.com/kitconcept/volto-light-theme/pull/610)
|
|
13
|
+
- Fix the layout of eventCalendar block. @iFlameing [#612](https://github.com/kitconcept/volto-light-theme/pull/612)
|
|
14
|
+
- Fix extra request in edit mode of event calendar block. @iFlameing [#613](https://github.com/kitconcept/volto-light-theme/pull/613)
|
|
15
|
+
- Fix extra request in view mode of event calendar block. @iFlameing [#615](https://github.com/kitconcept/volto-light-theme/pull/615)
|
|
22
16
|
|
|
23
17
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugins": {
|
|
3
|
+
"../../core/packages/scripts/prepublish.js": {}
|
|
4
|
+
},
|
|
5
|
+
"hooks": {
|
|
6
|
+
"after:bump": [
|
|
7
|
+
"pipx run towncrier build --draft --yes --version ${version} > .changelog.draft",
|
|
8
|
+
"pipx run towncrier build --yes --version ${version}",
|
|
9
|
+
"cp ../../README.md ./ && cp CHANGELOG.md ../../CHANGELOG.md",
|
|
10
|
+
"python3 -c 'import json; data = json.load(open(\"../../package.json\")); data[\"version\"] = \"${version}\"; json.dump(data, open(\"../../package.json\", \"w\"), indent=2)'",
|
|
11
|
+
"git add ../../CHANGELOG.md ../../package.json"
|
|
12
|
+
],
|
|
13
|
+
"after:release": "rm .changelog.draft README.md"
|
|
14
|
+
},
|
|
15
|
+
"npm": {
|
|
16
|
+
"publish": false
|
|
17
|
+
},
|
|
18
|
+
"git": {
|
|
19
|
+
"changelog": "pipx run towncrier build --draft --yes --version 0.0.0",
|
|
20
|
+
"requireUpstream": false,
|
|
21
|
+
"requireCleanWorkingDir": false,
|
|
22
|
+
"commitMessage": "Release ${version}",
|
|
23
|
+
"tagName": "${version}",
|
|
24
|
+
"tagAnnotation": "Release ${version}"
|
|
25
|
+
},
|
|
26
|
+
"github": {
|
|
27
|
+
"release": true,
|
|
28
|
+
"releaseName": "${version}",
|
|
29
|
+
"releaseNotes": "cat .changelog.draft"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- towncrier release notes start -->
|
|
10
10
|
|
|
11
|
+
## 7.0.0-alpha.16 (2025-07-23)
|
|
12
|
+
|
|
13
|
+
### Feature
|
|
14
|
+
|
|
15
|
+
- Add query support in eventCalendar Block. @iFlameing [#609](https://github.com/kitconcept/volto-light-theme/pull/609)
|
|
16
|
+
- Added support for the blocks configuration TTW behavior. @sneridagh [#614](https://github.com/kitconcept/volto-light-theme/pull/614)
|
|
17
|
+
- Update carousel block version, example content and cypress @iRohitSingh [#616](https://github.com/kitconcept/volto-light-theme/pull/616)
|
|
18
|
+
|
|
19
|
+
### Bugfix
|
|
20
|
+
|
|
21
|
+
- Fixed missing hide_description prop in the Summary component within
|
|
22
|
+
TeaserDefaultBodyTemplate and add cypress test for carousel block @iRohitSingh [#610](https://github.com/kitconcept/volto-light-theme/pull/610)
|
|
23
|
+
- Fix the layout of eventCalendar block. @iFlameing [#612](https://github.com/kitconcept/volto-light-theme/pull/612)
|
|
24
|
+
- Fix extra request in edit mode of event calendar block. @iFlameing [#613](https://github.com/kitconcept/volto-light-theme/pull/613)
|
|
25
|
+
- Fix extra request in view mode of event calendar block. @iFlameing [#615](https://github.com/kitconcept/volto-light-theme/pull/615)
|
|
26
|
+
|
|
11
27
|
## 7.0.0-alpha.15 (2025-07-17)
|
|
12
28
|
|
|
13
29
|
### Breaking
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitconcept/volto-light-theme",
|
|
3
|
-
"version": "7.0.0-alpha.
|
|
3
|
+
"version": "7.0.0-alpha.16",
|
|
4
4
|
"description": "Volto Light Theme by kitconcept",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@eeacms/volto-accordion-block": "^10.4.6",
|
|
60
60
|
"@kitconcept/volto-banner-block": "^1.0.1",
|
|
61
61
|
"@kitconcept/volto-button-block": "^4.0.0-alpha.0",
|
|
62
|
-
"@kitconcept/volto-carousel-block": "^2.0.0-alpha.
|
|
62
|
+
"@kitconcept/volto-carousel-block": "^2.0.0-alpha.3",
|
|
63
63
|
"@kitconcept/volto-dsgvo-banner": "^2.4.0",
|
|
64
64
|
"@kitconcept/volto-heading-block": "^2.4.0",
|
|
65
65
|
"@kitconcept/volto-highlight-block": "^4.1.0",
|
|
@@ -71,8 +71,8 @@ const SearchBlockEdit = (props) => {
|
|
|
71
71
|
onTriggerSearch(
|
|
72
72
|
'',
|
|
73
73
|
data?.facets,
|
|
74
|
-
data?.query?.sort_on,
|
|
75
|
-
data?.query?.sort_order,
|
|
74
|
+
data?.query?.sort_on || 'start',
|
|
75
|
+
data?.query?.sort_order || 'ascending',
|
|
76
76
|
);
|
|
77
77
|
}, [deepQuery, onTriggerSearch, data]);
|
|
78
78
|
|
|
@@ -18,20 +18,26 @@ import {
|
|
|
18
18
|
Text,
|
|
19
19
|
type ValidationResult,
|
|
20
20
|
} from 'react-aria-components';
|
|
21
|
+
import cx from 'classnames';
|
|
21
22
|
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
22
23
|
import CalendarSVG from '@plone/volto/icons/calendar.svg';
|
|
24
|
+
import ClearSVG from '@plone/volto/icons/clear.svg';
|
|
23
25
|
|
|
24
26
|
export interface DateRangePickerProps<T extends DateValue>
|
|
25
27
|
extends RACDateRangePickerProps<T> {
|
|
26
28
|
label?: string;
|
|
27
29
|
description?: string;
|
|
28
30
|
errorMessage?: string | ((validation: ValidationResult) => string);
|
|
31
|
+
onResetDateRange: () => void;
|
|
32
|
+
dateRange?: { start: DateValue; end: DateValue } | null;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export function DateRangePicker<T extends DateValue>({
|
|
32
36
|
label,
|
|
33
37
|
description,
|
|
34
38
|
errorMessage,
|
|
39
|
+
onResetDateRange,
|
|
40
|
+
dateRange,
|
|
35
41
|
...props
|
|
36
42
|
}: DateRangePickerProps<T>) {
|
|
37
43
|
return (
|
|
@@ -45,10 +51,17 @@ export function DateRangePicker<T extends DateValue>({
|
|
|
45
51
|
<DateInput slot="end">
|
|
46
52
|
{(segment) => <DateSegment segment={segment} />}
|
|
47
53
|
</DateInput>
|
|
48
|
-
<
|
|
54
|
+
<button
|
|
55
|
+
className={cx('reset-date-range', { visibility: dateRange?.start })}
|
|
56
|
+
onClick={onResetDateRange}
|
|
57
|
+
>
|
|
58
|
+
<Icon name={ClearSVG} color="#000" size="30px" />
|
|
59
|
+
</button>
|
|
60
|
+
<Button slot="trigger">
|
|
49
61
|
<Icon name={CalendarSVG} color="#000" />
|
|
50
62
|
</Button>
|
|
51
63
|
</Group>
|
|
64
|
+
|
|
52
65
|
{description && <Text slot="description">{description}</Text>}
|
|
53
66
|
<FieldError>{errorMessage}</FieldError>
|
|
54
67
|
<Popover>
|
|
@@ -4,7 +4,7 @@ import Card from '@kitconcept/volto-light-theme/primitives/Card/Card';
|
|
|
4
4
|
import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary';
|
|
5
5
|
import cx from 'classnames';
|
|
6
6
|
|
|
7
|
-
const EventItem = ({ item, lang }) => {
|
|
7
|
+
const EventItem = ({ item, lang, isEditMode }) => {
|
|
8
8
|
const formatter = new Intl.DateTimeFormat(lang, {
|
|
9
9
|
year: 'numeric',
|
|
10
10
|
month: 'short',
|
|
@@ -16,6 +16,7 @@ const EventItem = ({ item, lang }) => {
|
|
|
16
16
|
});
|
|
17
17
|
const start = item.start ? new Date(item.start) : null;
|
|
18
18
|
const end = item.end ? new Date(item.end) : null;
|
|
19
|
+
const notSameDay = end && start.getDate() !== end.getDate();
|
|
19
20
|
const formattedStartDate = start ? formatter.format(start) : '';
|
|
20
21
|
const formattedEndDate = end ? formatter.format(end) : '';
|
|
21
22
|
const formattedHeaderDate = !end
|
|
@@ -23,14 +24,14 @@ const EventItem = ({ item, lang }) => {
|
|
|
23
24
|
: headFormatter.formatRange(start, end);
|
|
24
25
|
return (
|
|
25
26
|
<div className="card-listing">
|
|
26
|
-
<Card href={item['@id']} className="event-card">
|
|
27
|
+
<Card href={isEditMode ? '' : item['@id']} className="event-card">
|
|
27
28
|
<Card.Image>
|
|
28
|
-
<div className={cx('date-inset', { 'has-end-date':
|
|
29
|
+
<div className={cx('date-inset', { 'has-end-date': notSameDay })}>
|
|
29
30
|
<div className="day">
|
|
30
31
|
{String(start.getDate()).padStart(2, '0')}
|
|
31
32
|
</div>
|
|
32
33
|
<div className="month">{formattedStartDate}</div>
|
|
33
|
-
{
|
|
34
|
+
{notSameDay && (
|
|
34
35
|
<>
|
|
35
36
|
<div className="separator"></div>
|
|
36
37
|
<div className="day">
|
|
@@ -60,7 +61,12 @@ const EventCalenderTemplate = (props) => {
|
|
|
60
61
|
return (
|
|
61
62
|
<div className="event-calendar items">
|
|
62
63
|
{props.items.map((item: any, index: number) => (
|
|
63
|
-
<EventItem
|
|
64
|
+
<EventItem
|
|
65
|
+
key={index}
|
|
66
|
+
item={item}
|
|
67
|
+
lang={lang}
|
|
68
|
+
isEditMode={props.isEditMode}
|
|
69
|
+
/>
|
|
64
70
|
))}
|
|
65
71
|
</div>
|
|
66
72
|
);
|
|
@@ -36,6 +36,7 @@ function getInitialState(
|
|
|
36
36
|
id,
|
|
37
37
|
sortOnParam,
|
|
38
38
|
sortOrderParam,
|
|
39
|
+
dateRangeQuery = [],
|
|
39
40
|
) {
|
|
40
41
|
const { types: facetWidgetTypes } =
|
|
41
42
|
config.blocks.blocksConfig.search.extensions.facetWidgets;
|
|
@@ -69,6 +70,7 @@ function getInitialState(
|
|
|
69
70
|
},
|
|
70
71
|
]
|
|
71
72
|
: []),
|
|
73
|
+
...(dateRangeQuery || []),
|
|
72
74
|
],
|
|
73
75
|
sort_on: sortOnParam || data.query?.sort_on,
|
|
74
76
|
sort_order: sortOrderParam || data.query?.sort_order,
|
|
@@ -88,6 +90,7 @@ function getInitialState(
|
|
|
88
90
|
*/
|
|
89
91
|
function normalizeState({
|
|
90
92
|
query, // base query
|
|
93
|
+
dateRangeQuery,
|
|
91
94
|
facets, // facet values
|
|
92
95
|
id, // block id
|
|
93
96
|
searchText, // SearchableText
|
|
@@ -156,6 +159,10 @@ function normalizeState({
|
|
|
156
159
|
});
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
if (dateRangeQuery) {
|
|
163
|
+
params.query.push(...dateRangeQuery);
|
|
164
|
+
}
|
|
165
|
+
|
|
159
166
|
return params;
|
|
160
167
|
}
|
|
161
168
|
|
|
@@ -272,8 +279,10 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
272
279
|
|
|
273
280
|
// TODO: refactor, should use only useLocationStateManager()!!!
|
|
274
281
|
const [searchText, setSearchText] = React.useState(urlSearchText);
|
|
282
|
+
const [dateRangeQuery, setDateRangeQuery] = React.useState([]);
|
|
275
283
|
|
|
276
284
|
const handleDateRangeChange = (query) => {
|
|
285
|
+
setDateRangeQuery(query);
|
|
277
286
|
setSearchData((prevSearchData) => {
|
|
278
287
|
const filteredQuery = prevSearchData.query?.filter(
|
|
279
288
|
(item) => item.i !== 'start' && item.i !== 'end',
|
|
@@ -380,8 +389,10 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
380
389
|
preventOverrideOfFacetState,
|
|
381
390
|
]);
|
|
382
391
|
|
|
383
|
-
const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
|
|
384
|
-
const [sortOrder, setSortOrder] = React.useState(
|
|
392
|
+
const [sortOn, setSortOn] = React.useState(data?.query?.sort_on || 'start');
|
|
393
|
+
const [sortOrder, setSortOrder] = React.useState(
|
|
394
|
+
data?.query?.sort_order || 'ascending',
|
|
395
|
+
);
|
|
385
396
|
|
|
386
397
|
const [searchData, setSearchData] = React.useState(
|
|
387
398
|
getInitialState(data, facets, urlSearchText, id),
|
|
@@ -398,9 +409,18 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
398
409
|
id,
|
|
399
410
|
sortOn,
|
|
400
411
|
sortOrder,
|
|
412
|
+
dateRangeQuery,
|
|
401
413
|
),
|
|
402
414
|
);
|
|
403
|
-
}, [
|
|
415
|
+
}, [
|
|
416
|
+
deepData,
|
|
417
|
+
deepFacets,
|
|
418
|
+
urlSearchText,
|
|
419
|
+
id,
|
|
420
|
+
sortOn,
|
|
421
|
+
sortOrder,
|
|
422
|
+
dateRangeQuery,
|
|
423
|
+
]);
|
|
404
424
|
|
|
405
425
|
const timeoutRef = React.useRef();
|
|
406
426
|
const facetSettings = data?.facets;
|
|
@@ -419,9 +439,10 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
419
439
|
const newSearchData = normalizeState({
|
|
420
440
|
id,
|
|
421
441
|
query: data.query || {},
|
|
442
|
+
dateRangeQuery: dateRangeQuery,
|
|
422
443
|
facets: toSearchFacets || facets,
|
|
423
444
|
searchText: toSearchText ? toSearchText.trim() : '',
|
|
424
|
-
sortOn: toSortOn ||
|
|
445
|
+
sortOn: toSortOn || sortOn,
|
|
425
446
|
sortOrder: toSortOrder || sortOrder,
|
|
426
447
|
facetSettings,
|
|
427
448
|
});
|
|
@@ -445,6 +466,7 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
445
466
|
sortOn,
|
|
446
467
|
sortOrder,
|
|
447
468
|
facetSettings,
|
|
469
|
+
dateRangeQuery,
|
|
448
470
|
],
|
|
449
471
|
);
|
|
450
472
|
|
|
@@ -82,13 +82,20 @@ const TopSideFacets = (props) => {
|
|
|
82
82
|
} = props;
|
|
83
83
|
const { showSearchButton } = data;
|
|
84
84
|
const isLive = !showSearchButton;
|
|
85
|
+
const [dateRange, setDateRange] = React.useState({ start: null, end: null });
|
|
85
86
|
const onhandleDateRangeChange = (value) => {
|
|
86
|
-
|
|
87
|
-
const
|
|
87
|
+
setDateRange(value);
|
|
88
|
+
const start = toJSDate(value?.start);
|
|
89
|
+
const end = toJSDate(value?.end);
|
|
88
90
|
const dateRangeQuery = getDateRangeIOV(start, end);
|
|
89
91
|
handleDateRangeChange(dateRangeQuery);
|
|
90
92
|
};
|
|
91
93
|
|
|
94
|
+
const onResetDateRange = () => {
|
|
95
|
+
setDateRange({ start: null, end: null });
|
|
96
|
+
handleDateRangeChange([]);
|
|
97
|
+
};
|
|
98
|
+
|
|
92
99
|
const FacetWrapper = ({ children }) => {
|
|
93
100
|
const colWidth = data.facets.length < 5 ? 12 / data.facets.length : 4;
|
|
94
101
|
return (
|
|
@@ -102,7 +109,12 @@ const TopSideFacets = (props) => {
|
|
|
102
109
|
<div className="search-block-event searchBlock-facets">
|
|
103
110
|
{data.headline && <h2 className="headline">{data.headline}</h2>}
|
|
104
111
|
<div className="first-row">
|
|
105
|
-
<DateRangePicker
|
|
112
|
+
<DateRangePicker
|
|
113
|
+
value={dateRange}
|
|
114
|
+
onChange={onhandleDateRangeChange}
|
|
115
|
+
onResetDateRange={onResetDateRange}
|
|
116
|
+
dateRange={dateRange}
|
|
117
|
+
/>
|
|
106
118
|
{/* <SearchDetails
|
|
107
119
|
text={searchedText}
|
|
108
120
|
total={totalItems}
|
|
@@ -210,6 +210,11 @@ const SearchSchema = ({ data = {}, intl }) => {
|
|
|
210
210
|
title: 'Default',
|
|
211
211
|
fields: ['headline'],
|
|
212
212
|
},
|
|
213
|
+
{
|
|
214
|
+
id: 'searchquery',
|
|
215
|
+
title: intl.formatMessage(messages.baseSearchQuery),
|
|
216
|
+
fields: ['query'],
|
|
217
|
+
},
|
|
213
218
|
{
|
|
214
219
|
id: 'facets',
|
|
215
220
|
title: intl.formatMessage(messages.facets),
|
|
@@ -272,6 +277,9 @@ const SearchSchema = ({ data = {}, intl }) => {
|
|
|
272
277
|
facetsTitle: {
|
|
273
278
|
title: intl.formatMessage(messages.sectionTitle),
|
|
274
279
|
},
|
|
280
|
+
query: {
|
|
281
|
+
title: 'Query',
|
|
282
|
+
},
|
|
275
283
|
},
|
|
276
284
|
required: [],
|
|
277
285
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
3
|
+
import { BlocksConfigMerger } from '../../helpers/BlocksConfigMerger';
|
|
4
|
+
import type { Content } from '@plone/types';
|
|
5
|
+
import type { MutatorDSL } from '../../types';
|
|
6
|
+
|
|
7
|
+
type FormState = {
|
|
8
|
+
content: {
|
|
9
|
+
data: Content;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ConfigInjector = () => {
|
|
14
|
+
const blockConfigData = useSelector<FormState, MutatorDSL>(
|
|
15
|
+
(state) =>
|
|
16
|
+
state.content.data?.['@components']?.inherit?.['kitconcept.blocks.config']
|
|
17
|
+
?.data?.blocks_config_mutator,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (blockConfigData) {
|
|
21
|
+
config.blocks.blocksConfig = BlocksConfigMerger(
|
|
22
|
+
config.blocks.blocksConfig,
|
|
23
|
+
blockConfigData,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// This component does not render anything, it just injects config from the Redux
|
|
28
|
+
// store in the global config
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default ConfigInjector;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button, Modal } from '@plone/components';
|
|
3
|
+
import { Dialog, DialogTrigger } from 'react-aria-components';
|
|
4
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
5
|
+
|
|
6
|
+
const BlockConfigJSONEditor = (props) => {
|
|
7
|
+
const [textValue, setTextValue] = React.useState(
|
|
8
|
+
JSON.stringify(props.value, null, 2),
|
|
9
|
+
);
|
|
10
|
+
const [error, setError] = React.useState('');
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<FormFieldWrapper
|
|
15
|
+
{...props}
|
|
16
|
+
error={error ? [error] : undefined}
|
|
17
|
+
className="block-config-json-editor"
|
|
18
|
+
>
|
|
19
|
+
<DialogTrigger>
|
|
20
|
+
<div className="button-wrapper">
|
|
21
|
+
<Button aria-label="Open configuration">Open configuration</Button>
|
|
22
|
+
</div>
|
|
23
|
+
<Modal className="block-config-json-editor-modal">
|
|
24
|
+
<Dialog>
|
|
25
|
+
{({ close }) => (
|
|
26
|
+
<div className="block-config-json-editor-dialog">
|
|
27
|
+
<textarea
|
|
28
|
+
value={textValue}
|
|
29
|
+
onChange={(e) => {
|
|
30
|
+
setTextValue(e.target.value);
|
|
31
|
+
try {
|
|
32
|
+
const validJSON = JSON.parse(e.target.value);
|
|
33
|
+
props.onChange(props.id, validJSON);
|
|
34
|
+
setError('');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
setError(`Invalid JSON: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
{error && (
|
|
41
|
+
<div className="block-config-json-editor-error">
|
|
42
|
+
{error}
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
<Button aria-label="Close" onPress={close}>
|
|
46
|
+
Close
|
|
47
|
+
</Button>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</Dialog>
|
|
51
|
+
</Modal>
|
|
52
|
+
</DialogTrigger>
|
|
53
|
+
</FormFieldWrapper>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default BlockConfigJSONEditor;
|
package/src/config/settings.ts
CHANGED
|
@@ -21,7 +21,7 @@ type apiExpanderInherit = {
|
|
|
21
21
|
|
|
22
22
|
export default function install(config: ConfigType) {
|
|
23
23
|
const EXPANDERS_INHERIT_BEHAVIORS =
|
|
24
|
-
'voltolighttheme.header,voltolighttheme.theme,voltolighttheme.footer,kitconcept.footer,kitconcept.sticky_menu';
|
|
24
|
+
'voltolighttheme.header,voltolighttheme.theme,voltolighttheme.footer,kitconcept.footer,kitconcept.sticky_menu,kitconcept.blocks.config';
|
|
25
25
|
config.settings.enableAutoBlockGroupingByBackgroundColor = true;
|
|
26
26
|
config.settings.navDepth = 3;
|
|
27
27
|
config.settings.slate.useLinkedHeadings = false;
|
package/src/config/slots.ts
CHANGED
|
@@ -7,6 +7,7 @@ import CoreFooter from '../components/Footer/slots/CoreFooter';
|
|
|
7
7
|
import StickyMenu from '../components/StickyMenu/StickyMenu';
|
|
8
8
|
import Anontools from '../components/Anontools/Anontools';
|
|
9
9
|
import type { Content } from '@plone/types';
|
|
10
|
+
import ConfigInjector from '../components/Theming/ConfigInjector';
|
|
10
11
|
|
|
11
12
|
export function hasInheritedBehavior(behavior: string) {
|
|
12
13
|
return ({ content }: { content: Content }) =>
|
|
@@ -19,6 +20,11 @@ export default function install(config: ConfigType) {
|
|
|
19
20
|
name: 'Theming',
|
|
20
21
|
component: Theming,
|
|
21
22
|
});
|
|
23
|
+
config.registerSlotComponent({
|
|
24
|
+
slot: 'aboveHeader',
|
|
25
|
+
name: 'ConfigInjector',
|
|
26
|
+
component: ConfigInjector,
|
|
27
|
+
});
|
|
22
28
|
|
|
23
29
|
config.registerSlotComponent({
|
|
24
30
|
slot: 'aboveHeader',
|
package/src/config/widgets.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { headerActionsSchema } from '../components/Widgets/schema/headerActionsS
|
|
|
11
11
|
import { footerLogosSchema } from '../components/Widgets/schema/footerLogosSchema';
|
|
12
12
|
import { footerLinksSchema } from '../components/Widgets/schema/footerLinksSchema';
|
|
13
13
|
import { iconLinkListSchema } from '../components/Widgets/schema/iconLinkListSchema';
|
|
14
|
+
import BlockConfigJSONEditor from '../components/Widgets/BlockConfigJSONEditor';
|
|
14
15
|
|
|
15
16
|
declare module '@plone/types' {
|
|
16
17
|
export interface WidgetsConfigById {
|
|
@@ -25,6 +26,7 @@ declare module '@plone/types' {
|
|
|
25
26
|
colorPicker: typeof ColorPicker;
|
|
26
27
|
blocksObject: typeof BlocksObject;
|
|
27
28
|
image: React.ComponentType;
|
|
29
|
+
blockConfigEditor: typeof BlockConfigJSONEditor;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -45,6 +47,8 @@ export default function install(config: ConfigType) {
|
|
|
45
47
|
config.widgets.widget.size = Size;
|
|
46
48
|
config.widgets.widget.themeColorSwatch = ThemeColorSwatch;
|
|
47
49
|
|
|
50
|
+
config.widgets.widget.blockConfigEditor = BlockConfigJSONEditor;
|
|
51
|
+
|
|
48
52
|
config.registerUtility({
|
|
49
53
|
name: 'headerActions',
|
|
50
54
|
type: 'schema',
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { BlocksConfigMerger } from './BlocksConfigMerger';
|
|
2
|
+
|
|
3
|
+
const baseBlocksConfig = {
|
|
4
|
+
teaser: {
|
|
5
|
+
restricted: false,
|
|
6
|
+
variations: [
|
|
7
|
+
{ id: 'variation1', label: 'Variation 1' },
|
|
8
|
+
{ id: 'variation2', label: 'Variation 2' },
|
|
9
|
+
{ id: 'variation3', label: 'Variation 3' },
|
|
10
|
+
],
|
|
11
|
+
themes: [],
|
|
12
|
+
},
|
|
13
|
+
gridBlock: {
|
|
14
|
+
restricted: false,
|
|
15
|
+
variations: [
|
|
16
|
+
{ id: 'variationA', label: 'Variation A' },
|
|
17
|
+
{ id: 'variationB', label: 'Variation B' },
|
|
18
|
+
],
|
|
19
|
+
themes: [],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const mutator = {
|
|
24
|
+
teaser: {
|
|
25
|
+
disable: true,
|
|
26
|
+
variations: ['variation1', 'variation2'],
|
|
27
|
+
themes: [
|
|
28
|
+
{
|
|
29
|
+
style: {
|
|
30
|
+
'--theme-color': '#fff',
|
|
31
|
+
'--theme-high-contrast-color': '#ecebeb',
|
|
32
|
+
'--theme-foreground-color': '#000',
|
|
33
|
+
'--theme-low-contrast-foreground-color': '#555555',
|
|
34
|
+
},
|
|
35
|
+
name: 'default',
|
|
36
|
+
label: 'Default',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
gridBlock: {
|
|
41
|
+
variations: ['variationB'],
|
|
42
|
+
},
|
|
43
|
+
description: {
|
|
44
|
+
disable: true,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe('BlocksConfigMerger', () => {
|
|
49
|
+
it('disables the block if disable is true', () => {
|
|
50
|
+
const result = BlocksConfigMerger(baseBlocksConfig, mutator);
|
|
51
|
+
expect(result.teaser.restricted).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('filters variations according to mutator', () => {
|
|
55
|
+
const result = BlocksConfigMerger(baseBlocksConfig, mutator);
|
|
56
|
+
expect(result.teaser.variations.map((v) => v.id)).toEqual([
|
|
57
|
+
'variation1',
|
|
58
|
+
'variation2',
|
|
59
|
+
]);
|
|
60
|
+
expect(result.gridBlock.variations.map((v) => v.id)).toEqual([
|
|
61
|
+
'variationB',
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('assigns themes from mutator', () => {
|
|
66
|
+
const result = BlocksConfigMerger(baseBlocksConfig, mutator);
|
|
67
|
+
expect(result.teaser.themes).toEqual([
|
|
68
|
+
{
|
|
69
|
+
style: {
|
|
70
|
+
'--theme-color': '#fff',
|
|
71
|
+
'--theme-high-contrast-color': '#ecebeb',
|
|
72
|
+
'--theme-foreground-color': '#000',
|
|
73
|
+
'--theme-low-contrast-foreground-color': '#555555',
|
|
74
|
+
},
|
|
75
|
+
name: 'default',
|
|
76
|
+
label: 'Default',
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not modify blocks not present in mutator', () => {
|
|
82
|
+
const result = BlocksConfigMerger(baseBlocksConfig, mutator);
|
|
83
|
+
expect(result.teaser.variations.length).toBe(2);
|
|
84
|
+
expect(result.gridBlock.restricted).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('ignores blocks in mutator that do not exist in blocksConfig', () => {
|
|
88
|
+
const result = BlocksConfigMerger(baseBlocksConfig, mutator);
|
|
89
|
+
expect(result.description).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('does not mutate the original blocksConfig', () => {
|
|
93
|
+
const original = JSON.parse(JSON.stringify(baseBlocksConfig));
|
|
94
|
+
BlocksConfigMerger(baseBlocksConfig, mutator);
|
|
95
|
+
expect(baseBlocksConfig).toEqual(original);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BlocksConfig } from '@plone/types';
|
|
2
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
3
|
+
import type { MutatorDSL } from '../types';
|
|
4
|
+
|
|
5
|
+
// Utility type for deep recursive Partial
|
|
6
|
+
type DeepPartial<T> = {
|
|
7
|
+
[P in keyof T]?: T[P] extends object
|
|
8
|
+
? T[P] extends Array<infer U>
|
|
9
|
+
? Array<DeepPartial<U>>
|
|
10
|
+
: DeepPartial<T[P]>
|
|
11
|
+
: T[P];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function BlocksConfigMerger(
|
|
15
|
+
blocksConfig: DeepPartial<BlocksConfig['blocksConfig']>,
|
|
16
|
+
merger: MutatorDSL,
|
|
17
|
+
): BlocksConfig['blocksConfig'] {
|
|
18
|
+
const mergedConfig = cloneDeep(blocksConfig);
|
|
19
|
+
|
|
20
|
+
Object.entries(merger).forEach(([blockId, dsl]) => {
|
|
21
|
+
if (!mergedConfig[blockId]) return;
|
|
22
|
+
|
|
23
|
+
// 1. Disable block
|
|
24
|
+
if (dsl.disable) {
|
|
25
|
+
mergedConfig[blockId]!.restricted = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Filter variations
|
|
29
|
+
if (Array.isArray(dsl.variations) && mergedConfig[blockId]!.variations) {
|
|
30
|
+
mergedConfig[blockId]!.variations = mergedConfig[
|
|
31
|
+
blockId
|
|
32
|
+
]!.variations!.filter((v) => dsl.variations!.includes(v.id));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Assign themes
|
|
36
|
+
if (Array.isArray(dsl.themes)) {
|
|
37
|
+
mergedConfig[blockId]!.themes = dsl.themes;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return mergedConfig as BlocksConfig['blocksConfig'];
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
SiteFooterSettings,
|
|
24
24
|
StickyMenuSettings,
|
|
25
25
|
PloneGobrSocialMediaSettings,
|
|
26
|
+
BlocksConfigSettings,
|
|
26
27
|
} from './types';
|
|
27
28
|
|
|
28
29
|
defineMessages({
|
|
@@ -58,6 +59,7 @@ declare module '@plone/types' {
|
|
|
58
59
|
'voltolighttheme.footer': CustomInheritBehavior<SiteFooterSettings>;
|
|
59
60
|
'kitconcept.sticky_menu': CustomInheritBehavior<StickyMenuSettings>;
|
|
60
61
|
'kitconcept.footer': CustomInheritBehavior<SiteFooterSettings>;
|
|
62
|
+
'kitconcept.blocks.config': CustomInheritBehavior<BlocksConfigSettings>;
|
|
61
63
|
'plonegovbr.socialmedia.settings': CustomInheritBehavior<PloneGobrSocialMediaSettings>;
|
|
62
64
|
};
|
|
63
65
|
}
|
package/src/theme/_layout.scss
CHANGED
|
@@ -369,7 +369,8 @@ External link removal for all the blocks.
|
|
|
369
369
|
.block-editor-teaser .card-inner, // deprecate when category is in place
|
|
370
370
|
.block-editor-slateTable .block.table,
|
|
371
371
|
.block-editor-highlight .teaser-description-title,
|
|
372
|
-
.block-editor-toc .table-of-contents
|
|
372
|
+
.block-editor-toc .table-of-contents,
|
|
373
|
+
.block-editor-eventCalendar .search-block-event {
|
|
373
374
|
@include default-container-width();
|
|
374
375
|
@include adjustMarginsToEditContainer($default-container-width);
|
|
375
376
|
}
|
package/src/theme/_widgets.scss
CHANGED
|
@@ -192,3 +192,63 @@ span.color-contrast-label {
|
|
|
192
192
|
margin-top: 10px;
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
+
|
|
196
|
+
.block-config-json-editor.field.inline {
|
|
197
|
+
.button-wrapper {
|
|
198
|
+
display: flex;
|
|
199
|
+
min-height: 60px;
|
|
200
|
+
justify-content: flex-end;
|
|
201
|
+
padding: 0;
|
|
202
|
+
|
|
203
|
+
button {
|
|
204
|
+
@include button-style;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.eight.wide.column:has(.button-wrapper) {
|
|
209
|
+
padding-right: 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.form-error-label {
|
|
213
|
+
text-align: end;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.block-config-json-editor-modal {
|
|
218
|
+
width: var(--narrow-block-width, 600px);
|
|
219
|
+
max-width: 600px;
|
|
220
|
+
border: 1px solid var(--border-color);
|
|
221
|
+
border-radius: 6px;
|
|
222
|
+
background: var(--overlay-background);
|
|
223
|
+
box-shadow: 0 8px 20px #0000001a;
|
|
224
|
+
color: var(--text-color);
|
|
225
|
+
outline: none;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.block-config-json-editor-dialog {
|
|
229
|
+
display: flex;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
|
|
232
|
+
button {
|
|
233
|
+
@include button-style;
|
|
234
|
+
padding: 20px 10px;
|
|
235
|
+
}
|
|
236
|
+
textarea {
|
|
237
|
+
display: block;
|
|
238
|
+
width: 100%;
|
|
239
|
+
height: 500px;
|
|
240
|
+
padding: 10px;
|
|
241
|
+
border-radius: 5px;
|
|
242
|
+
margin-bottom: $spacing-small;
|
|
243
|
+
font-family: monospace;
|
|
244
|
+
font-size: 14px;
|
|
245
|
+
line-height: 1.5;
|
|
246
|
+
resize: vertical;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.block-config-json-editor-error {
|
|
251
|
+
margin-bottom: $spacing-small;
|
|
252
|
+
color: #d01157 !important;
|
|
253
|
+
font-size: 14px;
|
|
254
|
+
}
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
.react-aria-DateInput[slot='start'] {
|
|
35
|
-
|
|
35
|
+
min-width: 117px;
|
|
36
|
+
margin-right: 15px;
|
|
36
37
|
outline-color: $black;
|
|
37
38
|
span {
|
|
38
39
|
color: $black;
|
|
@@ -42,8 +43,9 @@
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
.react-aria-DateInput[slot='end'] {
|
|
45
|
-
|
|
46
|
-
margin-
|
|
46
|
+
min-width: 117px;
|
|
47
|
+
margin-right: 5px;
|
|
48
|
+
margin-left: 15px;
|
|
47
49
|
outline-color: $black;
|
|
48
50
|
span {
|
|
49
51
|
color: $black;
|
|
@@ -65,6 +67,23 @@
|
|
|
65
67
|
min-width: 30px;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
70
|
+
|
|
71
|
+
.reset-date-range {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
padding: 0;
|
|
75
|
+
border: none;
|
|
76
|
+
margin-right: 5px;
|
|
77
|
+
background: none;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
visibility: hidden;
|
|
81
|
+
|
|
82
|
+
&.visibility {
|
|
83
|
+
pointer-events: auto;
|
|
84
|
+
visibility: visible;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
.search-details {
|
package/src/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Brain, Image } from '@plone/types';
|
|
1
|
+
import type { Brain, Image, StyleDefinition } from '@plone/types';
|
|
2
2
|
|
|
3
3
|
type hrefType = {
|
|
4
4
|
'@id': string;
|
|
@@ -87,6 +87,19 @@ export type PloneGobrSocialMediaSettings = {
|
|
|
87
87
|
social_links: Array<iconLink>;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
+
export type MutatorDSL = Record<
|
|
91
|
+
string,
|
|
92
|
+
{
|
|
93
|
+
disable?: boolean;
|
|
94
|
+
variations?: string[];
|
|
95
|
+
themes?: StyleDefinition[];
|
|
96
|
+
}
|
|
97
|
+
>;
|
|
98
|
+
|
|
99
|
+
export type BlocksConfigSettings = {
|
|
100
|
+
blocks_config_mutator: MutatorDSL;
|
|
101
|
+
};
|
|
102
|
+
|
|
90
103
|
export type CustomInheritBehavior<T> = {
|
|
91
104
|
data: T;
|
|
92
105
|
from: {
|
package/vitest.config.mjs
CHANGED
|
@@ -4,10 +4,13 @@ import path from 'path';
|
|
|
4
4
|
|
|
5
5
|
export default defineConfig({
|
|
6
6
|
...voltoVitestConfig,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
server: {
|
|
8
|
+
fs: {
|
|
9
|
+
allow: [
|
|
10
|
+
// Allow vite/vitest to access these folders
|
|
11
|
+
'..', // allow going up from frontend/
|
|
12
|
+
path.resolve(__dirname, '../../../../../core/packages/volto'),
|
|
13
|
+
],
|
|
11
14
|
},
|
|
12
15
|
},
|
|
13
16
|
});
|