@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 CHANGED
@@ -1,23 +1,17 @@
1
- ## 7.0.0-alpha.15 (2025-07-17)
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
- - Added block model v3 as opt-in. @sneridagh [#532](https://github.com/kitconcept/volto-light-theme/pull/532)
13
- - Update Spanish translation [@macagua] [#596](https://github.com/kitconcept/volto-light-theme/pull/596)
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 the use case where the sticky menu item is allowed to not have link. @sneridagh
18
-
19
- ### Internal
20
-
21
- - Improve listing template of Listing, Search and Grid block with Card Component. @iFlameing [#601](https://github.com/kitconcept/volto-light-theme/pull/601)
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.15",
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.1",
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
- <Button>
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': end })}>
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
- {end && (
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 key={index} item={item} lang={lang} />
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(data?.query?.sort_order);
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
- }, [deepData, deepFacets, urlSearchText, id, sortOn, sortOrder]);
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 || undefined,
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
- const start = toJSDate(value.start);
87
- const end = toJSDate(value.end);
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 onChange={onhandleDateRangeChange} />
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
  };
@@ -36,6 +36,7 @@ const TeaserDefaultTemplate = (props) => {
36
36
  <Summary
37
37
  item={!data.overwrite ? href : { ...href, ...filteredData }}
38
38
  HeadingTag="h2"
39
+ hide_description={props.data?.hide_description}
39
40
  />
40
41
  </Card.Summary>
41
42
  </Card>
@@ -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;
@@ -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;
@@ -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',
@@ -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
  }
@@ -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
  }
@@ -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
- margin-right: 10px;
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
- margin-right: $spacing-small;
46
- margin-left: 10px;
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
- resolve: {
8
- alias: {
9
- '@plone/volto': path.resolve(__dirname, '../../core/packages/volto/src'), // Add paths accordingly
10
- // 'promise-file-reader': require.resolve('promise-file-reader') // Add to identify dependency from package
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
  });