@kitconcept/volto-light-theme 8.0.0-alpha.5 → 8.0.0-alpha.7
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 +6 -8
- package/CHANGELOG.md +19 -0
- package/package.json +5 -4
- package/src/__mocks__/semantic-ui-react.ts +31 -0
- 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/Navigation/Navigation.test.tsx +176 -0
- package/src/components/Navigation/{Navigation.jsx → Navigation.tsx} +77 -37
- package/src/components/Tags/Tags.test.tsx +71 -0
- package/src/components/Tags/{Tags.jsx → Tags.tsx} +9 -25
- package/src/config/blocks.tsx +1 -0
- package/src/internalChecks.test.ts +94 -0
- package/src/theme/blocks/_accordion.scss +1 -0
- package/src/theme/notfound.scss +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/.changelog.draft
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
## 8.0.0-alpha.
|
|
2
|
-
|
|
3
|
-
### Feature
|
|
4
|
-
|
|
5
|
-
- Registry color definitions support for ColorSwatch widget. @sneridagh [#723](https://github.com/kitconcept/volto-light-theme/pull/723)
|
|
1
|
+
## 8.0.0-alpha.7 (2025-11-24)
|
|
6
2
|
|
|
7
3
|
### Bugfix
|
|
8
4
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
5
|
+
- Fix css of unauthorized Page @iRohitSingh [#unauthorizedpageCss](https://github.com/kitconcept/volto-light-theme/pull/unauthorizedpageCss)
|
|
6
|
+
- Fixed Accordion arrow/buttons no longer shrink when the title text becomes too long. @jnptk
|
|
11
7
|
|
|
12
8
|
### Internal
|
|
13
9
|
|
|
14
|
-
-
|
|
10
|
+
- Refactoring to TS some components. @sneridagh [#731](https://github.com/kitconcept/volto-light-theme/pull/731)
|
|
11
|
+
- Update to latest RAC to match the @plone/components one. @sneridagh
|
|
12
|
+
- Use forked Razzle. Update to use Volto 19a14. @sneridagh
|
|
15
13
|
|
|
16
14
|
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- towncrier release notes start -->
|
|
10
10
|
|
|
11
|
+
## 8.0.0-alpha.7 (2025-11-24)
|
|
12
|
+
|
|
13
|
+
### Bugfix
|
|
14
|
+
|
|
15
|
+
- Fix css of unauthorized Page @iRohitSingh [#unauthorizedpageCss](https://github.com/kitconcept/volto-light-theme/pull/unauthorizedpageCss)
|
|
16
|
+
- Fixed Accordion arrow/buttons no longer shrink when the title text becomes too long. @jnptk
|
|
17
|
+
|
|
18
|
+
### Internal
|
|
19
|
+
|
|
20
|
+
- Refactoring to TS some components. @sneridagh [#731](https://github.com/kitconcept/volto-light-theme/pull/731)
|
|
21
|
+
- Update to latest RAC to match the @plone/components one. @sneridagh
|
|
22
|
+
- Use forked Razzle. Update to use Volto 19a14. @sneridagh
|
|
23
|
+
|
|
24
|
+
## 8.0.0-alpha.6 (2025-11-13)
|
|
25
|
+
|
|
26
|
+
### Bugfix
|
|
27
|
+
|
|
28
|
+
- Missing @kitconcept/volto-bm3-compat as dep. @sneridagh [#726](https://github.com/kitconcept/volto-light-theme/pull/726)
|
|
29
|
+
|
|
11
30
|
## 8.0.0-alpha.5 (2025-11-11)
|
|
12
31
|
|
|
13
32
|
### Feature
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitconcept/volto-light-theme",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.7",
|
|
4
4
|
"description": "Volto Light Theme by kitconcept",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"release-it": "^19.0.3",
|
|
46
46
|
"typescript": "^5.7.3",
|
|
47
47
|
"vitest": "^3.1.2",
|
|
48
|
-
"@plone/types": "2.0.0-alpha.
|
|
48
|
+
"@plone/types": "2.0.0-alpha.11"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@dnd-kit/core": "6.0.8",
|
|
@@ -54,10 +54,11 @@
|
|
|
54
54
|
"embla-carousel-autoplay": "^8.0.0",
|
|
55
55
|
"embla-carousel-react": "^8.0.0",
|
|
56
56
|
"react-animate-height": "^3.2.3",
|
|
57
|
-
"react-aria-components": "^1.
|
|
57
|
+
"react-aria-components": "^1.13.0",
|
|
58
58
|
"react-colorful": "^5.6.1",
|
|
59
59
|
"uuid": "^11.0.0",
|
|
60
|
-
"@plone/components": "^4.0.0-alpha.
|
|
60
|
+
"@plone/components": "^4.0.0-alpha.4",
|
|
61
|
+
"@kitconcept/volto-bm3-compat": "^1.0.0-alpha.1"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
64
|
"classnames": "^2.5.1",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type GenericProps = {
|
|
4
|
+
children?: React.ReactNode;
|
|
5
|
+
} & Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
const createComponent =
|
|
8
|
+
(element: keyof JSX.IntrinsicElements = 'div') =>
|
|
9
|
+
({ children, ...rest }: GenericProps) =>
|
|
10
|
+
React.createElement(element, rest, children);
|
|
11
|
+
|
|
12
|
+
export const Container = createComponent('div');
|
|
13
|
+
export const Segment = createComponent('section');
|
|
14
|
+
export const Button = createComponent('button');
|
|
15
|
+
export const Form = createComponent('form');
|
|
16
|
+
export const Input = createComponent('input');
|
|
17
|
+
export const Pagination = createComponent('div');
|
|
18
|
+
export const Dimmer = createComponent('div');
|
|
19
|
+
export const Loader = createComponent('div');
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line import/no-anonymous-default-export
|
|
22
|
+
export default {
|
|
23
|
+
Container,
|
|
24
|
+
Segment,
|
|
25
|
+
Button,
|
|
26
|
+
Form,
|
|
27
|
+
Input,
|
|
28
|
+
Pagination,
|
|
29
|
+
Dimmer,
|
|
30
|
+
Loader,
|
|
31
|
+
};
|
|
@@ -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;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { render, screen, fireEvent } 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 Navigation from './Navigation';
|
|
9
|
+
import { getNavigation } from '@plone/volto/actions/navigation/navigation';
|
|
10
|
+
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
|
|
11
|
+
import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
|
|
12
|
+
|
|
13
|
+
vi.mock('../../helpers/doesNodeContainClick', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: () => false,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('@plone/volto/components/theme/Icon/Icon', () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: () => <span data-testid="icon" />,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('@plone/volto/components/theme/Navigation/NavItem', () => ({
|
|
24
|
+
__esModule: true,
|
|
25
|
+
default: ({ item }: { item: { title: string } }) => (
|
|
26
|
+
<span data-testid="nav-item">{item.title}</span>
|
|
27
|
+
),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('@plone/volto/actions/navigation/navigation', () => ({
|
|
31
|
+
getNavigation: vi.fn(() => ({ type: 'GET_NAVIGATION' })),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock('@plone/volto/helpers/Url/Url', () => ({
|
|
35
|
+
getBaseUrl: vi.fn((path: string) => path),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock('@plone/volto/helpers/Utils/Utils', () => ({
|
|
39
|
+
hasApiExpander: vi.fn(() => false),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock('@plone/volto/registry', () => ({
|
|
43
|
+
__esModule: true,
|
|
44
|
+
default: {
|
|
45
|
+
settings: {
|
|
46
|
+
navDepth: 3,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('@plone/volto/icons/clear.svg', () => ({
|
|
52
|
+
__esModule: true,
|
|
53
|
+
default: 'clear.svg',
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const mockStore = configureStore();
|
|
57
|
+
|
|
58
|
+
const mockedHasApiExpander = vi.mocked(hasApiExpander);
|
|
59
|
+
const mockedGetNavigation = vi.mocked(getNavigation);
|
|
60
|
+
const mockedGetBaseUrl = vi.mocked(getBaseUrl);
|
|
61
|
+
|
|
62
|
+
type TestNavigationItem = {
|
|
63
|
+
title: string;
|
|
64
|
+
url: string;
|
|
65
|
+
nav_title?: string;
|
|
66
|
+
items?: TestNavigationItem[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const baseItems: TestNavigationItem[] = [
|
|
70
|
+
{ title: 'About', url: '/about' },
|
|
71
|
+
{ title: 'News', url: '/news' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const renderNavigation = ({
|
|
75
|
+
items = baseItems,
|
|
76
|
+
hasFatMenuHeader = false,
|
|
77
|
+
formFatMenu,
|
|
78
|
+
pathname = '/news',
|
|
79
|
+
}: {
|
|
80
|
+
items?: TestNavigationItem[];
|
|
81
|
+
hasFatMenuHeader?: boolean;
|
|
82
|
+
formFatMenu?: boolean;
|
|
83
|
+
pathname?: string;
|
|
84
|
+
} = {}) => {
|
|
85
|
+
const store = mockStore({
|
|
86
|
+
content: {
|
|
87
|
+
data: {
|
|
88
|
+
'@components': {
|
|
89
|
+
inherit: {
|
|
90
|
+
'voltolighttheme.header': {
|
|
91
|
+
data: { has_fat_menu: hasFatMenuHeader },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
form: {
|
|
98
|
+
global:
|
|
99
|
+
formFatMenu === undefined ? undefined : { has_fat_menu: formFatMenu },
|
|
100
|
+
},
|
|
101
|
+
intl: {
|
|
102
|
+
locale: 'en',
|
|
103
|
+
},
|
|
104
|
+
userSession: {
|
|
105
|
+
token: null,
|
|
106
|
+
},
|
|
107
|
+
navigation: {
|
|
108
|
+
items,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const renderResult = render(
|
|
113
|
+
<Provider store={store}>
|
|
114
|
+
<MemoryRouter>
|
|
115
|
+
<Navigation pathname={pathname} />
|
|
116
|
+
</MemoryRouter>
|
|
117
|
+
</Provider>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return { store, ...renderResult };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
describe('Navigation', () => {
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
vi.clearAllMocks();
|
|
126
|
+
mockedHasApiExpander.mockReturnValue(false);
|
|
127
|
+
mockedGetNavigation.mockReturnValue({ type: 'GET_NAVIGATION' });
|
|
128
|
+
mockedGetBaseUrl.mockImplementation((path: string) => path);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('renders simple nav items when fat menu is disabled', () => {
|
|
132
|
+
renderNavigation({ hasFatMenuHeader: false });
|
|
133
|
+
|
|
134
|
+
const navItems = screen.getAllByTestId('nav-item');
|
|
135
|
+
expect(navItems).toHaveLength(baseItems.length);
|
|
136
|
+
expect(navItems[0]).toHaveTextContent('About');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('expands and collapses the fat menu', () => {
|
|
140
|
+
const { container } = renderNavigation({
|
|
141
|
+
hasFatMenuHeader: true,
|
|
142
|
+
items: [
|
|
143
|
+
{
|
|
144
|
+
title: 'Sections',
|
|
145
|
+
url: '/sections',
|
|
146
|
+
items: [
|
|
147
|
+
{
|
|
148
|
+
title: 'First',
|
|
149
|
+
url: '/sections/first',
|
|
150
|
+
items: [],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const openButtons = screen.getAllByRole('button', {
|
|
158
|
+
name: 'Open menu',
|
|
159
|
+
});
|
|
160
|
+
fireEvent.click(openButtons[0]);
|
|
161
|
+
expect(container.querySelector('.submenu.active')).not.toBeNull();
|
|
162
|
+
|
|
163
|
+
const closeButton = screen.getByLabelText('Close menu');
|
|
164
|
+
fireEvent.click(closeButton);
|
|
165
|
+
expect(container.querySelector('.submenu.active')).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('fetches navigation data when no expander is present', () => {
|
|
169
|
+
mockedGetBaseUrl.mockReturnValue('/base');
|
|
170
|
+
const { store } = renderNavigation({ pathname: '/blog' });
|
|
171
|
+
|
|
172
|
+
expect(mockedGetBaseUrl).toHaveBeenCalledWith('/blog');
|
|
173
|
+
expect(mockedGetNavigation).toHaveBeenCalledWith('/base', 3);
|
|
174
|
+
expect(store.getActions()).toEqual([{ type: 'GET_NAVIGATION' }]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
// SemanticUI-free pre-@plone/components
|
|
2
|
-
|
|
3
1
|
import React, { useState, useEffect, useRef } from 'react';
|
|
4
|
-
import PropTypes from 'prop-types';
|
|
5
2
|
import isEmpty from 'lodash/isEmpty';
|
|
6
3
|
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
|
7
4
|
import { NavLink } from 'react-router-dom';
|
|
8
5
|
import doesNodeContainClick from '../../helpers/doesNodeContainClick';
|
|
9
|
-
import { useIntl, defineMessages
|
|
6
|
+
import { useIntl, defineMessages } from 'react-intl';
|
|
10
7
|
import cx from 'classnames';
|
|
11
8
|
import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
|
|
12
9
|
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
|
|
@@ -28,31 +25,87 @@ const messages = defineMessages({
|
|
|
28
25
|
},
|
|
29
26
|
});
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
type NavigationItem = {
|
|
29
|
+
title: string;
|
|
30
|
+
nav_title?: string;
|
|
31
|
+
url: string;
|
|
32
|
+
items?: NavigationItem[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type NavigationProps = {
|
|
36
|
+
pathname: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type HeaderSettings = {
|
|
40
|
+
has_fat_menu?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type RootState = {
|
|
44
|
+
content: {
|
|
45
|
+
data?: {
|
|
46
|
+
'@components'?: {
|
|
47
|
+
inherit?: {
|
|
48
|
+
'voltolighttheme.header'?: {
|
|
49
|
+
data?: HeaderSettings;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
form: {
|
|
56
|
+
global?: {
|
|
57
|
+
has_fat_menu?: boolean;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
intl: {
|
|
62
|
+
locale: string;
|
|
63
|
+
};
|
|
64
|
+
userSession: {
|
|
65
|
+
token?: string | null;
|
|
66
|
+
};
|
|
67
|
+
navigation: {
|
|
68
|
+
items: NavigationItem[];
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const Navigation = ({ pathname }: NavigationProps) => {
|
|
73
|
+
const [desktopMenuOpen, setDesktopMenuOpen] = useState<number | null>(null);
|
|
74
|
+
const [currentOpenIndex, setCurrentOpenIndex] = useState<number | null>(null);
|
|
75
|
+
const navigation = useRef<HTMLElement | null>(null);
|
|
35
76
|
const dispatch = useDispatch();
|
|
36
77
|
const intl = useIntl();
|
|
37
78
|
const headerSettings = useSelector(
|
|
38
|
-
(state) =>
|
|
79
|
+
(state: RootState) =>
|
|
39
80
|
state.content.data?.['@components']?.inherit?.['voltolighttheme.header']
|
|
40
81
|
?.data,
|
|
41
82
|
);
|
|
42
|
-
const formData = useSelector((state) => state.form.global);
|
|
83
|
+
const formData = useSelector((state: RootState) => state.form.global);
|
|
43
84
|
|
|
44
|
-
const
|
|
45
|
-
!isEmpty(formData) && formData?.has_fat_menu
|
|
85
|
+
const hasFatMenuSetting =
|
|
86
|
+
!isEmpty(formData) && formData?.has_fat_menu !== undefined
|
|
46
87
|
? formData.has_fat_menu
|
|
47
88
|
: headerSettings?.has_fat_menu;
|
|
89
|
+
const hasFatMenu = hasFatMenuSetting ?? false;
|
|
90
|
+
|
|
91
|
+
const lang = useSelector((state: RootState) => state.intl.locale);
|
|
92
|
+
const token = useSelector(
|
|
93
|
+
(state: RootState) => state.userSession.token,
|
|
94
|
+
shallowEqual,
|
|
95
|
+
);
|
|
96
|
+
const items = useSelector(
|
|
97
|
+
(state: RootState) => state.navigation.items,
|
|
98
|
+
shallowEqual,
|
|
99
|
+
);
|
|
48
100
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
101
|
+
const closeMenu = () => {
|
|
102
|
+
setDesktopMenuOpen(null);
|
|
103
|
+
setCurrentOpenIndex(null);
|
|
104
|
+
};
|
|
52
105
|
|
|
53
106
|
useEffect(() => {
|
|
54
|
-
const handleClickOutside = (
|
|
55
|
-
if (navigation.current && doesNodeContainClick(navigation.current,
|
|
107
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
108
|
+
if (navigation.current && doesNodeContainClick(navigation.current, event))
|
|
56
109
|
return;
|
|
57
110
|
closeMenu();
|
|
58
111
|
};
|
|
@@ -70,11 +123,11 @@ const Navigation = ({ pathname }) => {
|
|
|
70
123
|
}
|
|
71
124
|
}, [pathname, token, dispatch]);
|
|
72
125
|
|
|
73
|
-
const isActive = (url) => {
|
|
126
|
+
const isActive = (url: string) => {
|
|
74
127
|
return (url === '' && pathname === '/') || (url !== '' && pathname === url);
|
|
75
128
|
};
|
|
76
129
|
|
|
77
|
-
const openMenu = (index) => {
|
|
130
|
+
const openMenu = (index: number) => {
|
|
78
131
|
if (index === currentOpenIndex) {
|
|
79
132
|
setDesktopMenuOpen(null);
|
|
80
133
|
setCurrentOpenIndex(null);
|
|
@@ -84,14 +137,9 @@ const Navigation = ({ pathname }) => {
|
|
|
84
137
|
}
|
|
85
138
|
};
|
|
86
139
|
|
|
87
|
-
const closeMenu = (index) => {
|
|
88
|
-
setDesktopMenuOpen(null);
|
|
89
|
-
setCurrentOpenIndex(null);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
140
|
useEffect(() => {
|
|
93
|
-
const handleEsc = (event) => {
|
|
94
|
-
if (event.keyCode === 27) {
|
|
141
|
+
const handleEsc = (event: KeyboardEvent) => {
|
|
142
|
+
if (event.key === 'Escape' || event.keyCode === 27) {
|
|
95
143
|
closeMenu();
|
|
96
144
|
}
|
|
97
145
|
};
|
|
@@ -113,7 +161,7 @@ const Navigation = ({ pathname }) => {
|
|
|
113
161
|
<ul className="desktop-menu">
|
|
114
162
|
{items.map((item, index) => (
|
|
115
163
|
<li key={item.url}>
|
|
116
|
-
{
|
|
164
|
+
{hasFatMenu ? (
|
|
117
165
|
<>
|
|
118
166
|
<button
|
|
119
167
|
onClick={() => openMenu(index)}
|
|
@@ -123,7 +171,7 @@ const Navigation = ({ pathname }) => {
|
|
|
123
171
|
(!desktopMenuOpen && pathname === item.url),
|
|
124
172
|
})}
|
|
125
173
|
aria-label={intl.formatMessage(messages.openFatMenu)}
|
|
126
|
-
aria-expanded={desktopMenuOpen === index
|
|
174
|
+
aria-expanded={desktopMenuOpen === index}
|
|
127
175
|
>
|
|
128
176
|
{item.title}
|
|
129
177
|
</button>
|
|
@@ -212,12 +260,4 @@ const Navigation = ({ pathname }) => {
|
|
|
212
260
|
);
|
|
213
261
|
};
|
|
214
262
|
|
|
215
|
-
|
|
216
|
-
pathname: PropTypes.string.isRequired,
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
Navigation.defaultProps = {
|
|
220
|
-
token: null,
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
export default injectIntl(Navigation);
|
|
263
|
+
export default Navigation;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, beforeEach, expect, vi } from 'vitest';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
+
|
|
6
|
+
import Tags from './Tags';
|
|
7
|
+
import type { Content } from '@plone/types';
|
|
8
|
+
|
|
9
|
+
const mockConfig = vi.hoisted(() => ({
|
|
10
|
+
getComponent: vi.fn(() => ({
|
|
11
|
+
component: ({ children }: { children: React.ReactNode }) => (
|
|
12
|
+
<div data-testid="container">{children}</div>
|
|
13
|
+
),
|
|
14
|
+
})),
|
|
15
|
+
settings: {
|
|
16
|
+
showTags: true,
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@plone/registry', () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
default: mockConfig,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const mockUseLiveData = vi.fn();
|
|
26
|
+
|
|
27
|
+
vi.mock('@kitconcept/volto-light-theme/helpers/useLiveData', () => ({
|
|
28
|
+
useLiveData: (content: unknown, behavior: unknown, field: string) =>
|
|
29
|
+
mockUseLiveData(content, behavior, field),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe('Tags', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
mockConfig.settings.showTags = true;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders tag links when enabled', () => {
|
|
39
|
+
mockUseLiveData.mockReturnValue(['React', 'Volto']);
|
|
40
|
+
render(
|
|
41
|
+
<MemoryRouter>
|
|
42
|
+
<Tags content={{ subjects: ['React', 'Volto'] } as Content} />
|
|
43
|
+
</MemoryRouter>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const links = screen.getAllByRole('link');
|
|
47
|
+
expect(links).toHaveLength(2);
|
|
48
|
+
expect(links[0]).toHaveAttribute('href', '/search?Subject=React');
|
|
49
|
+
expect(links[1]).toHaveTextContent('Volto');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns null when tags are hidden or empty', () => {
|
|
53
|
+
mockConfig.settings.showTags = false;
|
|
54
|
+
mockUseLiveData.mockReturnValue(['React']);
|
|
55
|
+
const { container: hidden } = render(
|
|
56
|
+
<MemoryRouter>
|
|
57
|
+
<Tags />
|
|
58
|
+
</MemoryRouter>,
|
|
59
|
+
);
|
|
60
|
+
expect(hidden.firstChild).toBeNull();
|
|
61
|
+
|
|
62
|
+
mockConfig.settings.showTags = true;
|
|
63
|
+
mockUseLiveData.mockReturnValue([]);
|
|
64
|
+
const { container: empty } = render(
|
|
65
|
+
<MemoryRouter>
|
|
66
|
+
<Tags />
|
|
67
|
+
</MemoryRouter>,
|
|
68
|
+
);
|
|
69
|
+
expect(empty.firstChild).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { Link } from 'react-router-dom';
|
|
2
|
-
import PropTypes from 'prop-types';
|
|
3
3
|
import { Container as SemanticContainer } from 'semantic-ui-react';
|
|
4
4
|
import config from '@plone/registry';
|
|
5
5
|
import { useLiveData } from '@kitconcept/volto-light-theme/helpers/useLiveData';
|
|
6
|
+
import type { Content } from '@plone/types';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
type TagsProps = {
|
|
9
|
+
content?: Content;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const Tags: React.FC<TagsProps> = ({ content }) => {
|
|
13
|
+
const safeContent = content ?? ({ subjects: [] } as Content);
|
|
14
|
+
const tags = useLiveData<string[]>(safeContent, undefined, 'subjects') || [];
|
|
9
15
|
const Container =
|
|
10
16
|
config.getComponent({ name: 'Container' }).component || SemanticContainer;
|
|
11
17
|
|
|
@@ -22,26 +28,4 @@ const Tags = ({ content }) => {
|
|
|
22
28
|
);
|
|
23
29
|
};
|
|
24
30
|
|
|
25
|
-
/**
|
|
26
|
-
* Property types.
|
|
27
|
-
* @property {Object} propTypes Property types.
|
|
28
|
-
* @static
|
|
29
|
-
*/
|
|
30
|
-
Tags.propTypes = {
|
|
31
|
-
content: PropTypes.shape({
|
|
32
|
-
subjects: PropTypes.arrayOf(PropTypes.string),
|
|
33
|
-
}),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Default properties.
|
|
38
|
-
* @property {Object} defaultProps Default properties.
|
|
39
|
-
* @static
|
|
40
|
-
*/
|
|
41
|
-
Tags.defaultProps = {
|
|
42
|
-
content: {
|
|
43
|
-
subjects: [],
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
31
|
export default Tags;
|
package/src/config/blocks.tsx
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
const recommendedAddonsPath = path.resolve(
|
|
10
|
+
__dirname,
|
|
11
|
+
'../../../..',
|
|
12
|
+
'recommendedAddons.json',
|
|
13
|
+
);
|
|
14
|
+
const mrsDeveloperPath = path.resolve(
|
|
15
|
+
__dirname,
|
|
16
|
+
'../../..',
|
|
17
|
+
'mrs.developer.json',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const readJSON = <T>(filePath: string): T =>
|
|
21
|
+
JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
22
|
+
|
|
23
|
+
const normalizeVersion = (version: unknown) =>
|
|
24
|
+
typeof version === 'string' ? version.replace(/^[\\^~]/, '') : version;
|
|
25
|
+
|
|
26
|
+
const excludedPackages = new Set(['@eeacms/volto-accordion-block']);
|
|
27
|
+
|
|
28
|
+
describe('internal checks', () => {
|
|
29
|
+
it('keeps recommended add-ons in sync with mrs.developer.json', () => {
|
|
30
|
+
const recommendedAddons = readJSON<Record<string, string>>(
|
|
31
|
+
recommendedAddonsPath,
|
|
32
|
+
);
|
|
33
|
+
const mrsDeveloper = readJSON<Record<string, unknown>>(mrsDeveloperPath);
|
|
34
|
+
|
|
35
|
+
const mrsByPackage = Object.entries(mrsDeveloper).reduce<
|
|
36
|
+
Record<string, Record<string, unknown> & { configKey: string }>
|
|
37
|
+
>((acc, [configKey, configValue]) => {
|
|
38
|
+
if (
|
|
39
|
+
configValue &&
|
|
40
|
+
typeof configValue === 'object' &&
|
|
41
|
+
'package' in configValue
|
|
42
|
+
) {
|
|
43
|
+
acc[String((configValue as Record<string, unknown>).package)] = {
|
|
44
|
+
...(configValue as Record<string, unknown>),
|
|
45
|
+
configKey,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return acc;
|
|
49
|
+
}, {});
|
|
50
|
+
|
|
51
|
+
const issues: string[] = [];
|
|
52
|
+
|
|
53
|
+
Object.entries(recommendedAddons).forEach(
|
|
54
|
+
([packageName, recommendedVersion]) => {
|
|
55
|
+
if (excludedPackages.has(packageName)) return;
|
|
56
|
+
|
|
57
|
+
const mrsEntry = mrsByPackage[packageName];
|
|
58
|
+
|
|
59
|
+
if (!mrsEntry) {
|
|
60
|
+
issues.push(
|
|
61
|
+
`${packageName} is listed in recommendedAddons.json but missing from frontend/mrs.developer.json.`,
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const mrsVersion =
|
|
67
|
+
(mrsEntry.tag as string | undefined) ||
|
|
68
|
+
(mrsEntry.branch as string | undefined) ||
|
|
69
|
+
(mrsEntry.version as string | undefined);
|
|
70
|
+
|
|
71
|
+
if (!mrsVersion) {
|
|
72
|
+
issues.push(
|
|
73
|
+
`${packageName} (${mrsEntry.configKey}) has no tag, branch, or version specified in frontend/mrs.developer.json.`,
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const normalizedRecommended = normalizeVersion(recommendedVersion);
|
|
79
|
+
const normalizedMrsVersion = normalizeVersion(mrsVersion);
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
recommendedVersion !== mrsVersion &&
|
|
83
|
+
normalizedRecommended !== normalizedMrsVersion
|
|
84
|
+
) {
|
|
85
|
+
issues.push(
|
|
86
|
+
`${packageName} differs: recommendedAddons.json=${recommendedVersion} vs frontend/mrs.developer.json(${mrsEntry.configKey})=${mrsVersion}.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(issues).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/theme/notfound.scss
CHANGED
package/vitest.config.mjs
CHANGED
|
@@ -10,6 +10,10 @@ export default defineConfig({
|
|
|
10
10
|
// Alias for absolute imports
|
|
11
11
|
'@kitconcept/volto-light-theme': path.resolve(__dirname, './src'),
|
|
12
12
|
'@kitconcept/volto-light-theme/': path.resolve(__dirname, './src/'),
|
|
13
|
+
'semantic-ui-react': path.resolve(
|
|
14
|
+
__dirname,
|
|
15
|
+
'./src/__mocks__/semantic-ui-react.ts',
|
|
16
|
+
),
|
|
13
17
|
},
|
|
14
18
|
},
|
|
15
19
|
server: {
|
package/razzle.extend.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
const plugins = (defaultPlugins) => {
|
|
2
|
-
const newPlugins = defaultPlugins.filter((plugin) => plugin !== 'scss');
|
|
3
|
-
newPlugins.push({
|
|
4
|
-
name: 'scss',
|
|
5
|
-
options: {
|
|
6
|
-
sass: {
|
|
7
|
-
dev: {
|
|
8
|
-
sassOptions: {
|
|
9
|
-
includePaths: ['node_modules'],
|
|
10
|
-
outputStyle: 'expanded',
|
|
11
|
-
sourceMap: true,
|
|
12
|
-
silenceDeprecations: ['legacy-js-api', 'mixed-decls', 'import'],
|
|
13
|
-
quietDeps: true,
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
prod: {
|
|
17
|
-
sassOptions: {
|
|
18
|
-
includePaths: ['node_modules'],
|
|
19
|
-
outputStyle: 'expanded',
|
|
20
|
-
sourceMap: true,
|
|
21
|
-
silenceDeprecations: ['legacy-js-api', 'mixed-decls', 'import'],
|
|
22
|
-
quietDeps: true,
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
return newPlugins;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const modify = (config, { target, dev }, webpack) => {
|
|
32
|
-
return config;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
module.exports = {
|
|
36
|
-
plugins,
|
|
37
|
-
modify,
|
|
38
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { addStyling } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
|
|
2
|
-
import { defineMessages } from 'react-intl';
|
|
3
|
-
import config from '@plone/volto/registry';
|
|
4
|
-
|
|
5
|
-
const messages = defineMessages({
|
|
6
|
-
backgroundColor: {
|
|
7
|
-
id: 'Background color',
|
|
8
|
-
defaultMessage: 'Background color',
|
|
9
|
-
},
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const defaultStylingSchema = ({ schema, formData, intl }) => {
|
|
13
|
-
const themes =
|
|
14
|
-
config.blocks?.blocksConfig?.[formData['@type']]?.themes ||
|
|
15
|
-
config.blocks.themes;
|
|
16
|
-
|
|
17
|
-
const defaultTheme =
|
|
18
|
-
config.blocks?.blocksConfig?.[formData['@type']]?.defaultTheme ||
|
|
19
|
-
// The default color is the first color in the themes list
|
|
20
|
-
config.blocks.themes?.[0].name;
|
|
21
|
-
|
|
22
|
-
addStyling({ schema, intl });
|
|
23
|
-
|
|
24
|
-
const stylingIndex = schema.fieldsets.findIndex(
|
|
25
|
-
(item) => item.id === 'styling',
|
|
26
|
-
);
|
|
27
|
-
schema.fieldsets[stylingIndex].fields = [
|
|
28
|
-
...schema.fieldsets[stylingIndex].fields,
|
|
29
|
-
'theme',
|
|
30
|
-
];
|
|
31
|
-
schema.properties.theme = {
|
|
32
|
-
widget: 'color_picker',
|
|
33
|
-
title: intl.formatMessage(messages.backgroundColor),
|
|
34
|
-
themes,
|
|
35
|
-
default: defaultTheme,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
return schema;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export const removeStylingSchema = ({ schema, formData, intl }) => {
|
|
42
|
-
schema.fieldsets = schema.fieldsets.filter((item) => item.id !== 'styling');
|
|
43
|
-
return schema;
|
|
44
|
-
};
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
// SemanticUI-free pre-@plone/components
|
|
2
|
-
import React, { Component } from 'react';
|
|
3
|
-
import PropTypes from 'prop-types';
|
|
4
|
-
import { connect } from 'react-redux';
|
|
5
|
-
import { compose } from 'redux';
|
|
6
|
-
import { Link } from 'react-router-dom';
|
|
7
|
-
import { defineMessages, injectIntl } from 'react-intl';
|
|
8
|
-
import { Container } from '@plone/components';
|
|
9
|
-
|
|
10
|
-
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
11
|
-
import { getBreadcrumbs } from '@plone/volto/actions/breadcrumbs/breadcrumbs';
|
|
12
|
-
import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
|
|
13
|
-
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
|
|
14
|
-
|
|
15
|
-
import homeSVG from '@plone/volto/icons/home.svg';
|
|
16
|
-
|
|
17
|
-
const messages = defineMessages({
|
|
18
|
-
home: {
|
|
19
|
-
id: 'Home',
|
|
20
|
-
defaultMessage: 'Home',
|
|
21
|
-
},
|
|
22
|
-
breadcrumbs: {
|
|
23
|
-
id: 'Breadcrumbs',
|
|
24
|
-
defaultMessage: 'Breadcrumbs',
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Breadcrumbs container class.
|
|
30
|
-
*/
|
|
31
|
-
export class BreadcrumbsComponent extends Component {
|
|
32
|
-
/**
|
|
33
|
-
* Property types.
|
|
34
|
-
* @property {Object} propTypes Property types.
|
|
35
|
-
* @static
|
|
36
|
-
*/
|
|
37
|
-
static propTypes = {
|
|
38
|
-
getBreadcrumbs: PropTypes.func.isRequired,
|
|
39
|
-
pathname: PropTypes.string.isRequired,
|
|
40
|
-
root: PropTypes.string,
|
|
41
|
-
items: PropTypes.arrayOf(
|
|
42
|
-
PropTypes.shape({
|
|
43
|
-
title: PropTypes.string,
|
|
44
|
-
url: PropTypes.string,
|
|
45
|
-
}),
|
|
46
|
-
).isRequired,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
componentDidMount() {
|
|
50
|
-
if (!hasApiExpander('breadcrumbs', getBaseUrl(this.props.pathname))) {
|
|
51
|
-
this.props.getBreadcrumbs(getBaseUrl(this.props.pathname));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Component will receive props
|
|
57
|
-
* @method componentWillReceiveProps
|
|
58
|
-
* @param {Object} nextProps Next properties
|
|
59
|
-
* @returns {undefined}
|
|
60
|
-
*/
|
|
61
|
-
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
62
|
-
if (nextProps.pathname !== this.props.pathname) {
|
|
63
|
-
if (!hasApiExpander('breadcrumbs', getBaseUrl(this.props.pathname))) {
|
|
64
|
-
this.props.getBreadcrumbs(getBaseUrl(nextProps.pathname));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Render method.
|
|
71
|
-
* @method render
|
|
72
|
-
* @returns {string} Markup for the component.
|
|
73
|
-
*/
|
|
74
|
-
render() {
|
|
75
|
-
return (
|
|
76
|
-
<div
|
|
77
|
-
role="navigation"
|
|
78
|
-
aria-label={this.props.intl.formatMessage(messages.breadcrumbs)}
|
|
79
|
-
className="breadcrumbs"
|
|
80
|
-
>
|
|
81
|
-
<Container layout>
|
|
82
|
-
<div className="breadcrumb">
|
|
83
|
-
<Link
|
|
84
|
-
to={this.props.root || '/'}
|
|
85
|
-
className="home"
|
|
86
|
-
title={this.props.intl.formatMessage(messages.home)}
|
|
87
|
-
>
|
|
88
|
-
<Icon name={homeSVG} size="25px" />
|
|
89
|
-
</Link>
|
|
90
|
-
{this.props.items.map((item, index, items) => [
|
|
91
|
-
<div className="divider" key={`divider-${item.url}`} />,
|
|
92
|
-
index < items.length - 1 ? (
|
|
93
|
-
<Link key={item.url} to={item.url} className="section">
|
|
94
|
-
{item.title}
|
|
95
|
-
</Link>
|
|
96
|
-
) : (
|
|
97
|
-
<div className="section active" key={item.url}>
|
|
98
|
-
{item.title}
|
|
99
|
-
</div>
|
|
100
|
-
),
|
|
101
|
-
])}
|
|
102
|
-
</div>
|
|
103
|
-
</Container>
|
|
104
|
-
</div>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export default compose(
|
|
110
|
-
injectIntl,
|
|
111
|
-
connect(
|
|
112
|
-
(state) => ({
|
|
113
|
-
items: state.breadcrumbs.items,
|
|
114
|
-
root: state.breadcrumbs.root,
|
|
115
|
-
}),
|
|
116
|
-
{ getBreadcrumbs },
|
|
117
|
-
),
|
|
118
|
-
)(BreadcrumbsComponent);
|