@kitconcept/volto-light-theme 8.0.0-alpha.2 → 8.0.0-alpha.21
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 +232 -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 +7 -4
- 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/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
|
@@ -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;
|
|
@@ -14,7 +14,7 @@ import arrowRightSVG from '@plone/volto/icons/right-key.svg';
|
|
|
14
14
|
import arrowLeftSVG from '@plone/volto/icons/left-key.svg';
|
|
15
15
|
import { MobileNavigationToggler } from './MobileNavigationToggler';
|
|
16
16
|
import { MobileToolsFooter } from './MobileToolsFooter';
|
|
17
|
-
|
|
17
|
+
import { useClient } from '@plone/volto/hooks/client/useClient';
|
|
18
18
|
const messages = defineMessages({
|
|
19
19
|
closeMobileMenu: {
|
|
20
20
|
id: 'Close menu',
|
|
@@ -43,7 +43,7 @@ const MenuItem = ({
|
|
|
43
43
|
pathname,
|
|
44
44
|
}) => {
|
|
45
45
|
const [isSubMenuOpen, setSubMenuOpen] = useState(false);
|
|
46
|
-
|
|
46
|
+
const isClient = useClient();
|
|
47
47
|
const openSubMenu = useCallback((e) => {
|
|
48
48
|
e.stopPropagation();
|
|
49
49
|
setSubMenuOpen(true);
|
|
@@ -68,9 +68,10 @@ const MenuItem = ({
|
|
|
68
68
|
);
|
|
69
69
|
const formData = useSelector((state) => state.form.global);
|
|
70
70
|
|
|
71
|
-
const has_intranet_header =
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const has_intranet_header =
|
|
72
|
+
isClient && !isEmpty(formData)
|
|
73
|
+
? formData.has_intranet_header
|
|
74
|
+
: headerSettings?.has_intranet_header;
|
|
74
75
|
|
|
75
76
|
return (
|
|
76
77
|
<li className={section.url === pathname ? 'current' : ''}>
|
|
@@ -154,16 +155,17 @@ const MobileNavigation = (props) => {
|
|
|
154
155
|
const currentLang = useSelector((state) => state.intl.locale);
|
|
155
156
|
const items = useSelector((state) => state.navigation.items || []);
|
|
156
157
|
const history = useHistory();
|
|
157
|
-
|
|
158
|
+
const isClient = useClient();
|
|
158
159
|
const headerSettings = useSelector(
|
|
159
160
|
(state) =>
|
|
160
161
|
state.content.data?.['@components']?.inherit?.['voltolighttheme.header']
|
|
161
162
|
?.data,
|
|
162
163
|
);
|
|
163
164
|
const formData = useSelector((state) => state.form.global);
|
|
164
|
-
const has_intranet_header =
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
const has_intranet_header =
|
|
166
|
+
isClient && !isEmpty(formData)
|
|
167
|
+
? formData.has_intranet_header
|
|
168
|
+
: headerSettings?.has_intranet_header;
|
|
167
169
|
|
|
168
170
|
const Footer = props.MobileToolsFooter || MobileToolsFooter;
|
|
169
171
|
|