@kitconcept/volto-light-theme 8.0.0-alpha.2 → 8.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changelog.draft +3 -4
- package/CHANGELOG.md +226 -0
- package/locales/de/LC_MESSAGES/volto.po +30 -115
- package/locales/en/LC_MESSAGES/volto.po +30 -115
- package/locales/es/LC_MESSAGES/volto.po +31 -116
- package/locales/eu/LC_MESSAGES/volto.po +58 -124
- package/locales/pt_BR/LC_MESSAGES/volto.po +38 -123
- package/locales/volto.pot +31 -116
- package/package.json +18 -15
- package/src/__mocks__/semantic-ui-react.ts +31 -0
- package/src/components/Blocks/Block/Edit.jsx +14 -6
- package/src/components/Blocks/Block/EditBlockWrapper.jsx +9 -3
- package/src/components/Blocks/Block/ErrorBoundary.test.tsx +55 -0
- package/src/components/Blocks/Block/ErrorBoundary.tsx +92 -0
- package/src/components/Blocks/Block/ErrorBoundaryMessage.tsx +66 -0
- package/src/components/Blocks/EventCalendar/Search/components/EventTemplate.tsx +1 -1
- package/src/components/Blocks/Image/Edit.jsx +1 -0
- package/src/components/Blocks/Listing/DefaultTemplate.jsx +12 -6
- package/src/components/Blocks/Listing/GridTemplate.jsx +16 -7
- package/src/components/Blocks/Listing/ListingBody.jsx +4 -1
- package/src/components/Blocks/Listing/SummaryTemplate.jsx +16 -7
- package/src/components/Blocks/Teaser/DefaultBody.tsx +25 -5
- package/src/components/Blocks/schema.ts +69 -0
- package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +128 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +117 -0
- package/src/components/Caption/Caption.test.tsx +31 -0
- package/src/components/Caption/{Caption.jsx → Caption.tsx} +14 -21
- package/src/components/Footer/ColumnLinks.tsx +2 -2
- package/src/components/Footer/slots/Colophon.tsx +13 -1
- package/src/components/Footer/slots/CoreFooter.tsx +4 -2
- package/src/components/Header/Header.tsx +3 -3
- package/src/components/LanguageSelector/LanguageSelector.tsx +91 -0
- package/src/components/MobileNavigation/MobileNavigation.jsx +11 -9
- package/src/components/Navigation/Navigation.test.tsx +176 -0
- package/src/components/Navigation/{Navigation.jsx → Navigation.tsx} +77 -37
- package/src/components/StickyMenu/MobileCarouselArrowButton.tsx +81 -0
- package/src/components/StickyMenu/MobileStickyMenu.tsx +76 -0
- package/src/components/Summary/DefaultSummary.tsx +10 -3
- package/src/components/Summary/EventSummary.tsx +10 -3
- package/src/components/Summary/FileSummary.tsx +10 -3
- package/src/components/Summary/NewsItemSummary.tsx +10 -3
- package/src/components/Summary/PersonSummary.tsx +10 -3
- package/src/components/Summary/Summary.stories.tsx +46 -30
- package/src/components/Tags/Tags.test.tsx +71 -0
- package/src/components/Tags/{Tags.jsx → Tags.tsx} +9 -25
- package/src/components/Theme/EventView.jsx +4 -4
- package/src/components/Theme/NewsItemView.jsx +4 -4
- package/src/components/Theme/RenderBlocks.jsx +45 -37
- package/src/components/Theme/RenderBlocksV2.jsx +51 -20
- package/src/components/Widgets/ColorSwatch.stories.tsx +197 -0
- package/src/components/Widgets/ColorSwatch.test.tsx +188 -0
- package/src/components/Widgets/ColorSwatch.tsx +77 -39
- package/src/components/Widgets/SoftTextWidget.tsx +129 -0
- package/src/components/Widgets/SoftTextareaWidget.tsx +118 -0
- package/src/components/Widgets/ThemeColorSwatch.tsx +5 -9
- package/src/config/blocks.tsx +21 -29
- package/src/config/slots.ts +7 -0
- package/src/config/widgets.ts +5 -9
- package/src/customizations/volto/components/manage/DragDropList/DragDropList.jsx +263 -0
- package/src/customizations/volto/components/theme/LanguageSelector/LanguageSelector.tsx +10 -0
- package/src/helpers/styleDefinitions.test.tsx +30 -0
- package/src/helpers/styleDefinitions.ts +49 -0
- package/src/icons/block_icn_accordion.svg +0 -0
- package/src/icons/block_icn_grid.svg +0 -0
- package/src/internalChecks.test.ts +94 -0
- package/src/primitives/Card/Card.stories.tsx +4 -1
- package/src/primitives/Card/Card.test.tsx +11 -33
- package/src/primitives/Card/Card.tsx +33 -43
- package/src/primitives/IconLinkList.tsx +53 -52
- package/src/theme/_bgcolor-blocks-layout.scss +43 -45
- package/src/theme/_content.scss +12 -13
- package/src/theme/_export_import.scss +94 -0
- package/src/theme/_footer.scss +64 -19
- package/src/theme/_header.scss +21 -4
- package/src/theme/_insets.scss +1 -1
- package/src/theme/_layout.scss +34 -15
- package/src/theme/_mobile-sticky-menu.scss +92 -0
- package/src/theme/_search-page.scss +249 -0
- package/src/theme/_typo-custom.scss +16 -5
- package/src/theme/_variables.scss +19 -4
- package/src/theme/_widgets.scss +15 -27
- package/src/theme/blocks/_accordion.scss +11 -4
- package/src/theme/blocks/_grid.scss +9 -77
- package/src/theme/blocks/_listing.scss +60 -126
- package/src/theme/blocks/_search.scss +3 -4
- package/src/theme/blocks/_table.scss +1 -0
- package/src/theme/blocks/_teaser.scss +7 -117
- package/src/theme/blocks/error-boundary.scss +11 -0
- package/src/theme/card.scss +107 -70
- package/src/theme/main.scss +5 -0
- package/src/theme/notfound.scss +27 -0
- package/src/theme/person.scss +28 -12
- package/src/theme/sticky-menu.scss +7 -5
- package/src/types.d.ts +1 -0
- package/vitest.config.mjs +4 -0
- package/razzle.extend.js +0 -38
- package/src/components/Blocks/schema.js +0 -44
- package/src/components/Breadcrumbs/Breadcrumbs.jsx +0 -118
- package/src/components/Widgets/AlignWidget.tsx +0 -84
- package/src/components/Widgets/BlockAlignment.tsx +0 -88
- package/src/components/Widgets/BlockWidth.tsx +0 -101
- package/src/components/Widgets/Buttons.tsx +0 -144
- package/src/components/Widgets/Size.tsx +0 -78
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { useSelector } from 'react-redux';
|
|
2
|
+
import type { GetSiteResponse } from '@plone/types';
|
|
1
3
|
import { isInternalURL } from '@plone/volto/helpers/Url/Url';
|
|
2
4
|
import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary';
|
|
3
5
|
import type { SummaryComponentType } from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary';
|
|
4
6
|
import Card from '../../../primitives/Card/Card';
|
|
5
7
|
import config from '@plone/volto/registry';
|
|
6
8
|
|
|
9
|
+
type FormState = {
|
|
10
|
+
site: { data: GetSiteResponse };
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
const TeaserDefaultTemplate = (props) => {
|
|
14
|
+
const site = useSelector<FormState, GetSiteResponse>(
|
|
15
|
+
(state) => state.site?.data,
|
|
16
|
+
);
|
|
17
|
+
const hideProfileLinks = site?.['kitconcept.disable_profile_links'];
|
|
8
18
|
const { data, isEditMode } = props;
|
|
9
19
|
const href = data.href?.[0] || {};
|
|
10
20
|
const image = data.preview_image?.[0];
|
|
@@ -15,25 +25,35 @@ const TeaserDefaultTemplate = (props) => {
|
|
|
15
25
|
name: 'Summary',
|
|
16
26
|
dependencies: [href['@type']],
|
|
17
27
|
}).component || DefaultSummary) as SummaryComponentType;
|
|
18
|
-
|
|
28
|
+
let showLink = !Summary.hideLink && !isEditMode;
|
|
29
|
+
if (href['@type'] === 'Person' && hideProfileLinks !== undefined) {
|
|
30
|
+
showLink = !hideProfileLinks && !isEditMode;
|
|
31
|
+
}
|
|
19
32
|
const { openExternalLinkInNewTab } = config.settings;
|
|
20
33
|
const openLinkInNewTab =
|
|
21
34
|
data.openLinkInNewTab ||
|
|
22
35
|
(openExternalLinkInNewTab && !isInternalURL(href['@id']));
|
|
23
|
-
|
|
36
|
+
|
|
37
|
+
// Ensures that overridden fields are used when "overwrite" is true
|
|
38
|
+
// and fallbacks to empty strings if they are not provided to ensure no undefined
|
|
39
|
+
// values are passed
|
|
40
|
+
const localOverrides = {
|
|
41
|
+
title: data.title || '',
|
|
42
|
+
description: data.description || '',
|
|
43
|
+
head_title: data.head_title || '',
|
|
44
|
+
};
|
|
24
45
|
|
|
25
46
|
return (
|
|
26
47
|
<Card item={showLink ? href : null} openLinkInNewTab={openLinkInNewTab}>
|
|
27
48
|
<Card.Image
|
|
28
49
|
src={url && !image?.image_field ? url : undefined}
|
|
29
|
-
item={!data.overwrite ? href : { ...href, ...
|
|
50
|
+
item={!data.overwrite ? href : { ...href, ...localOverrides }}
|
|
30
51
|
image={data.overwrite ? image : undefined}
|
|
31
52
|
imageComponent={Image}
|
|
32
53
|
/>
|
|
33
54
|
<Card.Summary>
|
|
34
55
|
<Summary
|
|
35
|
-
item={!data.overwrite ? href : { ...href, ...
|
|
36
|
-
HeadingTag="h2"
|
|
56
|
+
item={!data.overwrite ? href : { ...href, ...localOverrides }}
|
|
37
57
|
hide_description={props.data?.hide_description}
|
|
38
58
|
/>
|
|
39
59
|
</Card.Summary>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { addStyling } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
|
|
2
|
+
import { defineMessages } from 'react-intl';
|
|
3
|
+
import config from '@plone/volto/registry';
|
|
4
|
+
import type {
|
|
5
|
+
BlocksFormData,
|
|
6
|
+
JSONSchema,
|
|
7
|
+
SchemaEnhancerArgs,
|
|
8
|
+
StyleDefinition,
|
|
9
|
+
} from '@plone/types';
|
|
10
|
+
|
|
11
|
+
const messages = defineMessages({
|
|
12
|
+
backgroundColor: {
|
|
13
|
+
id: 'Background color',
|
|
14
|
+
defaultMessage: 'Background color',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const getThemes = (formData: BlocksFormData): StyleDefinition[] | undefined => {
|
|
19
|
+
const blockConfig = config.blocks?.blocksConfig?.[formData['@type']];
|
|
20
|
+
|
|
21
|
+
return blockConfig?.themes || config.blocks.themes;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const getDefaultTheme = (formData: BlocksFormData): string | undefined => {
|
|
25
|
+
const blockConfig = config.blocks?.blocksConfig?.[formData['@type']];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
blockConfig?.defaultTheme || config.blocks.themes?.[0]?.name || undefined
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const defaultStylingSchema = ({
|
|
33
|
+
schema,
|
|
34
|
+
formData,
|
|
35
|
+
intl,
|
|
36
|
+
}: SchemaEnhancerArgs): JSONSchema => {
|
|
37
|
+
const themes = getThemes(formData);
|
|
38
|
+
const defaultTheme = getDefaultTheme(formData);
|
|
39
|
+
|
|
40
|
+
addStyling({ schema, intl });
|
|
41
|
+
|
|
42
|
+
const stylingIndex = schema.fieldsets.findIndex(
|
|
43
|
+
(item) => item.id === 'styling',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (stylingIndex === -1 || !schema.fieldsets[stylingIndex]) {
|
|
47
|
+
return schema;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const stylingFieldset = schema.fieldsets[stylingIndex];
|
|
51
|
+
|
|
52
|
+
stylingFieldset.fields = [...stylingFieldset.fields, 'theme'];
|
|
53
|
+
|
|
54
|
+
schema.properties.theme = {
|
|
55
|
+
widget: 'color_picker',
|
|
56
|
+
title: intl.formatMessage(messages.backgroundColor),
|
|
57
|
+
themes,
|
|
58
|
+
default: defaultTheme,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return schema;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const removeStylingSchema = ({
|
|
65
|
+
schema,
|
|
66
|
+
}: Pick<SchemaEnhancerArgs, 'schema'>): JSONSchema => {
|
|
67
|
+
schema.fieldsets = schema.fieldsets.filter((item) => item.id !== 'styling');
|
|
68
|
+
return schema;
|
|
69
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import configureStore from 'redux-mock-store';
|
|
5
|
+
import { Provider } from 'react-intl-redux';
|
|
6
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
7
|
+
|
|
8
|
+
import Breadcrumbs from './Breadcrumbs';
|
|
9
|
+
import { getBreadcrumbs } from '@plone/volto/actions/breadcrumbs/breadcrumbs';
|
|
10
|
+
import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
|
|
11
|
+
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
|
|
12
|
+
|
|
13
|
+
vi.mock('@plone/components', () => ({
|
|
14
|
+
Container: ({ children }: { children: React.ReactNode }) => (
|
|
15
|
+
<div data-testid="container">{children}</div>
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('@plone/volto/components/theme/Icon/Icon', () => ({
|
|
20
|
+
__esModule: true,
|
|
21
|
+
default: () => <span data-testid="home-icon" />,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('@plone/volto/actions/breadcrumbs/breadcrumbs', () => ({
|
|
25
|
+
getBreadcrumbs: vi.fn(() => ({ type: 'GET_BREADCRUMBS' })),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('@plone/volto/helpers/Url/Url', () => ({
|
|
29
|
+
getBaseUrl: vi.fn(() => '/base'),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('@plone/volto/helpers/Utils/Utils', () => ({
|
|
33
|
+
hasApiExpander: vi.fn(() => false),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const mockStore = configureStore<{
|
|
37
|
+
breadcrumbs: {
|
|
38
|
+
items: { title: string; url: string }[];
|
|
39
|
+
root?: string;
|
|
40
|
+
};
|
|
41
|
+
intl: {
|
|
42
|
+
locale: string;
|
|
43
|
+
messages: Record<string, string>;
|
|
44
|
+
};
|
|
45
|
+
}>();
|
|
46
|
+
|
|
47
|
+
const mockedGetBreadcrumbs = vi.mocked(getBreadcrumbs);
|
|
48
|
+
const mockedGetBaseUrl = vi.mocked(getBaseUrl);
|
|
49
|
+
const mockedHasApiExpander = vi.mocked(hasApiExpander);
|
|
50
|
+
|
|
51
|
+
const renderBreadcrumbs = (
|
|
52
|
+
options: {
|
|
53
|
+
pathname?: string;
|
|
54
|
+
items?: { title: string; url: string }[];
|
|
55
|
+
root?: string;
|
|
56
|
+
} = {},
|
|
57
|
+
) => {
|
|
58
|
+
const {
|
|
59
|
+
pathname = '/current-path',
|
|
60
|
+
items = [
|
|
61
|
+
{ title: 'Section one', url: '/section-one' },
|
|
62
|
+
{ title: 'Current page', url: '/section-one/current' },
|
|
63
|
+
],
|
|
64
|
+
root = '/home',
|
|
65
|
+
} = options;
|
|
66
|
+
|
|
67
|
+
const store = mockStore({
|
|
68
|
+
breadcrumbs: {
|
|
69
|
+
items,
|
|
70
|
+
root,
|
|
71
|
+
},
|
|
72
|
+
intl: {
|
|
73
|
+
locale: 'en',
|
|
74
|
+
messages: {},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const renderResult = render(
|
|
79
|
+
<Provider store={store}>
|
|
80
|
+
<MemoryRouter>
|
|
81
|
+
<Breadcrumbs pathname={pathname} />
|
|
82
|
+
</MemoryRouter>
|
|
83
|
+
</Provider>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return { store, ...renderResult };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
describe('Breadcrumbs', () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.clearAllMocks();
|
|
92
|
+
mockedGetBaseUrl.mockReturnValue('/base');
|
|
93
|
+
mockedGetBreadcrumbs.mockReturnValue({ type: 'GET_BREADCRUMBS' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('renders the home link and breadcrumb items', () => {
|
|
97
|
+
mockedHasApiExpander.mockReturnValue(true);
|
|
98
|
+
const { container } = renderBreadcrumbs();
|
|
99
|
+
|
|
100
|
+
const homeLink = screen.getByTitle('Home') as HTMLAnchorElement;
|
|
101
|
+
expect(homeLink).toHaveAttribute('href', '/home');
|
|
102
|
+
expect(screen.getByText('Section one')).toBeInTheDocument();
|
|
103
|
+
expect(screen.getByText('Current page')).toHaveClass('section', {
|
|
104
|
+
exact: false,
|
|
105
|
+
});
|
|
106
|
+
expect(container.querySelector('.section.active')?.textContent).toBe(
|
|
107
|
+
'Current page',
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('dispatches breadcrumb fetching when no expander is registered', () => {
|
|
112
|
+
mockedHasApiExpander.mockReturnValue(false);
|
|
113
|
+
mockedGetBaseUrl.mockReturnValue('/base-url');
|
|
114
|
+
const { store } = renderBreadcrumbs({ pathname: '/another-path' });
|
|
115
|
+
|
|
116
|
+
expect(mockedGetBaseUrl).toHaveBeenCalledWith('/another-path');
|
|
117
|
+
expect(mockedGetBreadcrumbs).toHaveBeenCalledWith('/base-url');
|
|
118
|
+
expect(store.getActions()).toEqual([{ type: 'GET_BREADCRUMBS' }]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('skips fetching when breadcrumbs expander already exists', () => {
|
|
122
|
+
mockedHasApiExpander.mockReturnValue(true);
|
|
123
|
+
const { store } = renderBreadcrumbs();
|
|
124
|
+
|
|
125
|
+
expect(mockedGetBreadcrumbs).not.toHaveBeenCalled();
|
|
126
|
+
expect(store.getActions()).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type TypedUseSelectorHook,
|
|
4
|
+
useDispatch,
|
|
5
|
+
useSelector,
|
|
6
|
+
} from 'react-redux';
|
|
7
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
8
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
9
|
+
import { Container } from '@plone/components';
|
|
10
|
+
|
|
11
|
+
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
12
|
+
import { getBreadcrumbs } from '@plone/volto/actions/breadcrumbs/breadcrumbs';
|
|
13
|
+
import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
|
|
14
|
+
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
|
|
15
|
+
|
|
16
|
+
import homeSVG from '@plone/volto/icons/home.svg';
|
|
17
|
+
|
|
18
|
+
const messages = defineMessages({
|
|
19
|
+
home: {
|
|
20
|
+
id: 'Home',
|
|
21
|
+
defaultMessage: 'Home',
|
|
22
|
+
},
|
|
23
|
+
breadcrumbs: {
|
|
24
|
+
id: 'Breadcrumbs',
|
|
25
|
+
defaultMessage: 'Breadcrumbs',
|
|
26
|
+
},
|
|
27
|
+
controlpanel: {
|
|
28
|
+
id: 'Site Setup',
|
|
29
|
+
defaultMessage: 'Site Setup',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
type BreadcrumbItem = {
|
|
34
|
+
title: string;
|
|
35
|
+
url: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type BreadcrumbsProps = {
|
|
39
|
+
pathname: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type BreadcrumbState = {
|
|
43
|
+
items: BreadcrumbItem[];
|
|
44
|
+
root?: string | null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type RootState = {
|
|
48
|
+
breadcrumbs: BreadcrumbState;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const useRootSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
52
|
+
|
|
53
|
+
export const BreadcrumbsComponent = ({ pathname }: BreadcrumbsProps) => {
|
|
54
|
+
const intl = useIntl();
|
|
55
|
+
const dispatch = useDispatch();
|
|
56
|
+
const { pathname: realPath } = useLocation();
|
|
57
|
+
const controlpanelItems = useMemo(
|
|
58
|
+
() => [
|
|
59
|
+
{
|
|
60
|
+
url: '/controlpanel',
|
|
61
|
+
title: intl.formatMessage(messages.controlpanel),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
[intl],
|
|
65
|
+
);
|
|
66
|
+
const storeItems = useRootSelector((state) => state.breadcrumbs.items);
|
|
67
|
+
const root = useRootSelector((state) => state.breadcrumbs.root);
|
|
68
|
+
const items = realPath.startsWith('/controlpanel')
|
|
69
|
+
? controlpanelItems
|
|
70
|
+
: storeItems;
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const baseUrl = getBaseUrl(pathname);
|
|
74
|
+
|
|
75
|
+
if (!hasApiExpander('breadcrumbs', baseUrl)) {
|
|
76
|
+
dispatch(getBreadcrumbs(baseUrl));
|
|
77
|
+
}
|
|
78
|
+
}, [dispatch, pathname]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
role="navigation"
|
|
83
|
+
aria-label={intl.formatMessage(messages.breadcrumbs)}
|
|
84
|
+
className="breadcrumbs"
|
|
85
|
+
>
|
|
86
|
+
<Container layout>
|
|
87
|
+
<div className="breadcrumb">
|
|
88
|
+
<Link
|
|
89
|
+
to={root || '/'}
|
|
90
|
+
className="home"
|
|
91
|
+
title={intl.formatMessage(messages.home)}
|
|
92
|
+
>
|
|
93
|
+
<Icon name={homeSVG} size="25px" />
|
|
94
|
+
</Link>
|
|
95
|
+
{items.map((item, index) => {
|
|
96
|
+
const isLast = index === items.length - 1;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<React.Fragment key={item.url}>
|
|
100
|
+
<div className="divider" />
|
|
101
|
+
{isLast ? (
|
|
102
|
+
<div className="section active">{item.title}</div>
|
|
103
|
+
) : (
|
|
104
|
+
<Link to={item.url} className="section">
|
|
105
|
+
{item.title}
|
|
106
|
+
</Link>
|
|
107
|
+
)}
|
|
108
|
+
</React.Fragment>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</div>
|
|
112
|
+
</Container>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default BreadcrumbsComponent;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import Caption from './Caption';
|
|
6
|
+
|
|
7
|
+
describe('Caption', () => {
|
|
8
|
+
it('renders title, description lines, and credits', () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<Caption
|
|
11
|
+
title="My image"
|
|
12
|
+
description={'First line\n\nThird line'}
|
|
13
|
+
credit="Photo: Someone"
|
|
14
|
+
/>,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(screen.getByText('My image')).toBeInTheDocument();
|
|
18
|
+
const paragraphs = Array.from(
|
|
19
|
+
container.querySelectorAll('.description p'),
|
|
20
|
+
).map((node) => node.textContent);
|
|
21
|
+
expect(paragraphs).toEqual(['First line', '\u00A0', 'Third line']);
|
|
22
|
+
expect(screen.getByText('Photo: Someone')).toHaveClass('credits');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('allows configuring the wrapper tag', () => {
|
|
26
|
+
const { container } = render(<Caption as="div" title="Custom tag" />);
|
|
27
|
+
const wrapper = container.querySelector('div');
|
|
28
|
+
expect(wrapper).not.toBeNull();
|
|
29
|
+
expect(wrapper?.tagName).toBe('DIV');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
type CaptionProps = {
|
|
2
|
+
as?: keyof JSX.IntrinsicElements;
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
credit?: string;
|
|
6
|
+
};
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Image/video caption component class.
|
|
@@ -14,10 +14,14 @@ import PropTypes from 'prop-types';
|
|
|
14
14
|
* @params {object} credit Credit text.
|
|
15
15
|
* @returns {string} Markup of the component.
|
|
16
16
|
*/
|
|
17
|
-
const Caption = ({
|
|
18
|
-
|
|
17
|
+
const Caption = ({
|
|
18
|
+
as: Tag = 'figcaption',
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
credit,
|
|
22
|
+
}: CaptionProps) => {
|
|
19
23
|
return (
|
|
20
|
-
<
|
|
24
|
+
<Tag>
|
|
21
25
|
{title && <strong className="title">{title}</strong>}
|
|
22
26
|
{description && (
|
|
23
27
|
<div className="description">
|
|
@@ -27,19 +31,8 @@ const Caption = ({ as = 'figcaption', title, description, credit }) => {
|
|
|
27
31
|
</div>
|
|
28
32
|
)}
|
|
29
33
|
{credit && <p className="credits">{credit}</p>}
|
|
30
|
-
</
|
|
34
|
+
</Tag>
|
|
31
35
|
);
|
|
32
36
|
};
|
|
33
37
|
|
|
34
|
-
/**
|
|
35
|
-
* Property types.
|
|
36
|
-
* @property {Object} propTypes Property types.
|
|
37
|
-
* @static
|
|
38
|
-
*/
|
|
39
|
-
Caption.propTypes = {
|
|
40
|
-
credit: PropTypes.string,
|
|
41
|
-
title: PropTypes.string,
|
|
42
|
-
description: PropTypes.string,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
38
|
export default Caption;
|
|
@@ -14,8 +14,8 @@ const ColumnLinks = (props: ColumnLinksProps) => {
|
|
|
14
14
|
? links.map((link) => {
|
|
15
15
|
if (isEmpty(link) || !link.href) return null;
|
|
16
16
|
|
|
17
|
-
const title = link
|
|
18
|
-
const href = flattenToAppURL(link
|
|
17
|
+
const title = link?.title || link?.href[0]?.['title'];
|
|
18
|
+
const href = flattenToAppURL(link?.href[0]?.['@id']);
|
|
19
19
|
|
|
20
20
|
if (!href) return null;
|
|
21
21
|
|
|
@@ -2,8 +2,11 @@ import { Container } from '@plone/components';
|
|
|
2
2
|
import Copyright from './Copyright';
|
|
3
3
|
import Logo from '../../Logo/Logo';
|
|
4
4
|
import type { Content } from '@plone/types';
|
|
5
|
+
import isString from 'lodash/isString';
|
|
5
6
|
import { useLiveData } from '@kitconcept/volto-light-theme/helpers/useLiveData';
|
|
6
7
|
import type { SiteFooterSettings, SiteHeaderSettings } from '../../../types';
|
|
8
|
+
import config from '@plone/volto/registry';
|
|
9
|
+
import { createParagraph } from '@plone/volto-slate/utils';
|
|
7
10
|
|
|
8
11
|
const Colophon = ({ content }: { content: Content }) => {
|
|
9
12
|
const logo = useLiveData<SiteHeaderSettings['logo']>(
|
|
@@ -12,14 +15,23 @@ const Colophon = ({ content }: { content: Content }) => {
|
|
|
12
15
|
'logo',
|
|
13
16
|
);
|
|
14
17
|
|
|
18
|
+
const getValue = (value) => {
|
|
19
|
+
// Previously this was a text field
|
|
20
|
+
if (isString(value)) {
|
|
21
|
+
return [createParagraph(value)];
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
|
|
15
26
|
const footer_colophon_text = useLiveData<
|
|
16
27
|
SiteFooterSettings['footer_colophon_text']
|
|
17
28
|
>(content, 'voltolighttheme.footer', 'footer_colophon_text');
|
|
29
|
+
const RenderSlateToHtml = config.widgets.views.widget.slate_richtext;
|
|
18
30
|
|
|
19
31
|
return (
|
|
20
32
|
<Container className="colophon">
|
|
21
33
|
{footer_colophon_text ? (
|
|
22
|
-
|
|
34
|
+
<RenderSlateToHtml value={getValue(footer_colophon_text)} />
|
|
23
35
|
) : (
|
|
24
36
|
<>
|
|
25
37
|
<div className="powered-by">
|
|
@@ -38,8 +38,10 @@ const CoreFooter = ({ content }: { content: Content }) => {
|
|
|
38
38
|
<Container className="default">
|
|
39
39
|
<div className="footer-grid">
|
|
40
40
|
<div className="address-column">
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
<div className="logo">
|
|
42
|
+
{/* @ts-ignore the Logo definition is different from the Volto one */}
|
|
43
|
+
<Logo isFooterLogo />
|
|
44
|
+
</div>
|
|
43
45
|
<p
|
|
44
46
|
style={{ whiteSpace: 'pre-line' }}
|
|
45
47
|
dangerouslySetInnerHTML={{
|
|
@@ -82,7 +82,7 @@ const InternetHeader = ({ pathname, content }) => {
|
|
|
82
82
|
header_actions.map((item) => (
|
|
83
83
|
<UniversalLink
|
|
84
84
|
key={item['@id']}
|
|
85
|
-
href={item.href?.[0]['@id']}
|
|
85
|
+
href={item.href?.[0]?.['@id']}
|
|
86
86
|
openLinkInNewTab={item.openInNewTab}
|
|
87
87
|
>
|
|
88
88
|
{item.title}
|
|
@@ -162,7 +162,7 @@ const IntranetHeader = ({ pathname, content }) => {
|
|
|
162
162
|
|
|
163
163
|
return (
|
|
164
164
|
<>
|
|
165
|
-
<div className="header">
|
|
165
|
+
<div className="header header-intranet">
|
|
166
166
|
<div className="tools-wrapper">
|
|
167
167
|
<LanguageSelector />
|
|
168
168
|
|
|
@@ -174,7 +174,7 @@ const IntranetHeader = ({ pathname, content }) => {
|
|
|
174
174
|
header_actions.map((item) => (
|
|
175
175
|
<UniversalLink
|
|
176
176
|
key={item['@id']}
|
|
177
|
-
href={item.href?.[0]['@id']}
|
|
177
|
+
href={item.href?.[0]?.['@id']}
|
|
178
178
|
openLinkInNewTab={item.openInNewTab}
|
|
179
179
|
>
|
|
180
180
|
{item.title}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
|
|
3
|
+
import { useSelector } from 'react-redux';
|
|
4
|
+
import cx from 'classnames';
|
|
5
|
+
|
|
6
|
+
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
7
|
+
import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
|
|
8
|
+
import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
9
|
+
import { toReactIntlLang } from '@plone/volto/helpers/Utils/Utils';
|
|
10
|
+
|
|
11
|
+
import { defineMessages, useIntl, type IntlShape } from 'react-intl';
|
|
12
|
+
import type {
|
|
13
|
+
Content,
|
|
14
|
+
GetSiteResponse,
|
|
15
|
+
GetTranslationResponse,
|
|
16
|
+
} from '@plone/types';
|
|
17
|
+
|
|
18
|
+
const messages = defineMessages({
|
|
19
|
+
switchLanguageTo: {
|
|
20
|
+
id: 'Switch to',
|
|
21
|
+
defaultMessage: 'Switch to',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
type FormState = {
|
|
26
|
+
content: {
|
|
27
|
+
data: Content;
|
|
28
|
+
};
|
|
29
|
+
intl: IntlShape;
|
|
30
|
+
site: {
|
|
31
|
+
data: GetSiteResponse;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const LanguageSelector = ({
|
|
36
|
+
onClickAction = () => {},
|
|
37
|
+
}: {
|
|
38
|
+
onClickAction?: () => void;
|
|
39
|
+
}) => {
|
|
40
|
+
const intl = useIntl();
|
|
41
|
+
const currentLang = useSelector<FormState, IntlShape['locale']>(
|
|
42
|
+
(state) => state.intl.locale,
|
|
43
|
+
);
|
|
44
|
+
const translations = useSelector<
|
|
45
|
+
FormState,
|
|
46
|
+
GetTranslationResponse['items'] | undefined
|
|
47
|
+
>((state) => state.content.data?.['@components']?.translations?.items);
|
|
48
|
+
const isMultilingual = useSelector<
|
|
49
|
+
FormState,
|
|
50
|
+
GetSiteResponse['features']['multilingual']
|
|
51
|
+
>((state) => state.site.data.features?.multilingual);
|
|
52
|
+
const availableLanguages = useSelector<
|
|
53
|
+
FormState,
|
|
54
|
+
GetSiteResponse['plone.available_languages']
|
|
55
|
+
>((state) => state.site.data?.['plone.available_languages']);
|
|
56
|
+
|
|
57
|
+
return isMultilingual ? (
|
|
58
|
+
<div className="language-selector">
|
|
59
|
+
{availableLanguages?.map((lang) => {
|
|
60
|
+
const langKey = lang as keyof typeof langmap;
|
|
61
|
+
const translation = translations?.find(
|
|
62
|
+
(t: { language: string }) => t.language === lang,
|
|
63
|
+
);
|
|
64
|
+
return (
|
|
65
|
+
<Link
|
|
66
|
+
aria-label={`${intl.formatMessage(
|
|
67
|
+
messages.switchLanguageTo,
|
|
68
|
+
)} ${(langmap[langKey]?.nativeName || langKey).toLowerCase()}`}
|
|
69
|
+
className={cx({ selected: toReactIntlLang(lang) === currentLang })}
|
|
70
|
+
to={translation ? flattenToAppURL(translation['@id']) : `/${lang}`}
|
|
71
|
+
title={langmap[langKey]?.nativeName || langKey}
|
|
72
|
+
onClick={() => {
|
|
73
|
+
onClickAction();
|
|
74
|
+
}}
|
|
75
|
+
key={`language-selector-${lang}`}
|
|
76
|
+
>
|
|
77
|
+
{/* START CUSTOMIZATION */}
|
|
78
|
+
{langKey.slice(0, 2)}
|
|
79
|
+
{/* END CUSTOMIZATION} */}
|
|
80
|
+
</Link>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</div>
|
|
84
|
+
) : (
|
|
85
|
+
<Helmet>
|
|
86
|
+
<html lang={toReactIntlLang(currentLang)} />
|
|
87
|
+
</Helmet>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default LanguageSelector;
|