@kitconcept/volto-light-theme 8.0.0-alpha.3 → 8.0.0-alpha.30
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 -9
- package/CHANGELOG.md +310 -0
- package/locales/af/LC_MESSAGES/volto.po +645 -0
- package/locales/ar/LC_MESSAGES/volto.po +645 -0
- package/locales/bg/LC_MESSAGES/volto.po +645 -0
- package/locales/bn/LC_MESSAGES/volto.po +645 -0
- package/locales/ca/LC_MESSAGES/volto.po +645 -0
- package/locales/cs/LC_MESSAGES/volto.po +645 -0
- package/locales/cy/LC_MESSAGES/volto.po +645 -0
- package/locales/da/LC_MESSAGES/volto.po +645 -0
- package/locales/de/LC_MESSAGES/volto.po +83 -167
- package/locales/el/LC_MESSAGES/volto.po +645 -0
- package/locales/en/LC_MESSAGES/volto.po +30 -115
- package/locales/en_AU/LC_MESSAGES/volto.po +645 -0
- package/locales/en_GB/LC_MESSAGES/volto.po +645 -0
- package/locales/eo/LC_MESSAGES/volto.po +645 -0
- package/locales/es/LC_MESSAGES/volto.po +75 -160
- package/locales/et/LC_MESSAGES/volto.po +645 -0
- package/locales/eu/LC_MESSAGES/volto.po +59 -125
- package/locales/fa/LC_MESSAGES/volto.po +645 -0
- package/locales/fi/LC_MESSAGES/volto.po +645 -0
- package/locales/fr/LC_MESSAGES/volto.po +645 -0
- package/locales/fu/LC_MESSAGES/volto.po +645 -0
- package/locales/ga/LC_MESSAGES/volto.po +645 -0
- package/locales/gl/LC_MESSAGES/volto.po +645 -0
- package/locales/he/LC_MESSAGES/volto.po +645 -0
- package/locales/hi/LC_MESSAGES/volto.po +645 -0
- package/locales/hr/LC_MESSAGES/volto.po +645 -0
- package/locales/hu/LC_MESSAGES/volto.po +645 -0
- package/locales/hy/LC_MESSAGES/volto.po +645 -0
- package/locales/id/LC_MESSAGES/volto.po +645 -0
- package/locales/it/LC_MESSAGES/volto.po +645 -0
- package/locales/ja/LC_MESSAGES/volto.po +645 -0
- package/locales/ka/LC_MESSAGES/volto.po +645 -0
- package/locales/kn/LC_MESSAGES/volto.po +645 -0
- package/locales/ko/LC_MESSAGES/volto.po +645 -0
- package/locales/lt/LC_MESSAGES/volto.po +645 -0
- package/locales/lv/LC_MESSAGES/volto.po +645 -0
- package/locales/mi/LC_MESSAGES/volto.po +645 -0
- package/locales/mk_MK/LC_MESSAGES/volto.po +645 -0
- package/locales/ms/LC_MESSAGES/volto.po +645 -0
- package/locales/mt/LC_MESSAGES/volto.po +645 -0
- package/locales/my/LC_MESSAGES/volto.po +645 -0
- package/locales/nl/LC_MESSAGES/volto.po +645 -0
- package/locales/nl_BE/LC_MESSAGES/volto.po +645 -0
- package/locales/nn/LC_MESSAGES/volto.po +645 -0
- package/locales/no/LC_MESSAGES/volto.po +645 -0
- package/locales/pl/LC_MESSAGES/volto.po +645 -0
- package/locales/pt/LC_MESSAGES/volto.po +645 -0
- package/locales/pt_BR/LC_MESSAGES/volto.po +38 -123
- package/locales/rm/LC_MESSAGES/volto.po +645 -0
- package/locales/ro/LC_MESSAGES/volto.po +645 -0
- package/locales/ru/LC_MESSAGES/volto.po +645 -0
- package/locales/sk/LC_MESSAGES/volto.po +645 -0
- package/locales/sl/LC_MESSAGES/volto.po +645 -0
- package/locales/sm/LC_MESSAGES/volto.po +645 -0
- package/locales/sq/LC_MESSAGES/volto.po +645 -0
- package/locales/sr/LC_MESSAGES/volto.po +645 -0
- package/locales/sr_Cyrl/LC_MESSAGES/volto.po +645 -0
- package/locales/sr_Latn/LC_MESSAGES/volto.po +645 -0
- package/locales/sv/LC_MESSAGES/volto.po +645 -0
- package/locales/sw/LC_MESSAGES/volto.po +645 -0
- package/locales/ta/LC_MESSAGES/volto.po +645 -0
- package/locales/te/LC_MESSAGES/volto.po +645 -0
- package/locales/th/LC_MESSAGES/volto.po +645 -0
- package/locales/tl/LC_MESSAGES/volto.po +645 -0
- package/locales/to/LC_MESSAGES/volto.po +645 -0
- package/locales/tr/LC_MESSAGES/volto.po +645 -0
- package/locales/uk/LC_MESSAGES/volto.po +645 -0
- package/locales/vi/LC_MESSAGES/volto.po +645 -0
- package/locales/volto.pot +31 -116
- package/locales/zh_CN/LC_MESSAGES/volto.po +645 -0
- package/locales/zh_HK/LC_MESSAGES/volto.po +645 -0
- package/locales/zh_TW/LC_MESSAGES/volto.po +645 -0
- package/package.json +7 -4
- package/src/__mocks__/semantic-ui-react.ts +31 -0
- package/src/components/Blocks/Block/EditBlockWrapper.jsx +9 -3
- package/src/components/Blocks/Button/schema.js +12 -0
- package/src/components/Blocks/EventCalendar/Search/components/EventTemplate.tsx +1 -1
- package/src/components/Blocks/Image/Edit.jsx +9 -32
- package/src/components/Blocks/Image/View.jsx +9 -26
- package/src/components/Blocks/Image/adapter.js +28 -14
- package/src/components/Blocks/Image/adapter.test.js +156 -0
- package/src/components/Blocks/Image/schema.js +21 -7
- package/src/components/Blocks/Listing/DefaultTemplate.jsx +12 -6
- package/src/components/Blocks/Listing/GridTemplate.jsx +17 -7
- package/src/components/Blocks/Listing/ListingBody.jsx +4 -1
- package/src/components/Blocks/Listing/SummaryTemplate.jsx +17 -7
- package/src/components/Blocks/Maps/MapsSidebar.jsx +68 -0
- package/src/components/Blocks/Maps/View.jsx +37 -0
- package/src/components/Blocks/Maps/adapter.js +27 -0
- package/src/components/Blocks/Maps/adapter.test.js +63 -0
- package/src/components/Blocks/Maps/schema.js +42 -2
- package/src/components/Blocks/Separator/schema.js +12 -0
- package/src/components/Blocks/Teaser/DefaultBody.tsx +35 -6
- package/src/components/Blocks/Video/VideoSidebar.jsx +68 -0
- package/src/components/Blocks/Video/View.jsx +38 -0
- package/src/components/Blocks/Video/adapter.js +28 -0
- package/src/components/Blocks/Video/adapter.test.js +63 -0
- package/src/components/Blocks/Video/schema.js +42 -2
- 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/Footer.tsx +2 -2
- package/src/components/Footer/slots/Colophon.tsx +13 -1
- package/src/components/Footer/slots/CoreFooter.tsx +4 -2
- package/src/components/Footer/slots/FollowUsLogoAndLinks.tsx +12 -23
- 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} +89 -42
- 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/ImageView.jsx +8 -1
- package/src/components/Theme/NewsItemView.jsx +4 -4
- package/src/components/Theme/RenderBlocksV2.jsx +38 -15
- 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/ObjectList.tsx +37 -27
- 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 +83 -28
- package/src/config/classExtenders.ts +11 -10
- package/src/config/settings.ts +6 -0
- package/src/config/slots.ts +7 -0
- package/src/config/widgets.ts +5 -9
- package/src/customizations/volto/components/manage/Blocks/Maps/MapsSidebar.jsx +10 -0
- package/src/customizations/volto/components/manage/Blocks/Maps/View.jsx +10 -0
- package/src/customizations/volto/components/manage/Blocks/Video/VideoSidebar.jsx +10 -0
- package/src/customizations/volto/components/manage/Blocks/Video/View.jsx +10 -0
- 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/helpers/useLiveData.ts +7 -2
- package/src/index.ts +15 -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 +37 -44
- package/src/primitives/IconLinkList.tsx +53 -52
- package/src/primitives/LinkIconButton.tsx +52 -0
- package/src/reducers/errorContext.ts +14 -0
- package/src/reducers/index.ts +7 -0
- package/src/theme/_bgcolor-blocks-layout.scss +48 -46
- package/src/theme/_content.scss +12 -13
- package/src/theme/_export_import.scss +94 -0
- package/src/theme/_footer.scss +131 -64
- package/src/theme/_header.scss +25 -5
- package/src/theme/_insets.scss +1 -1
- package/src/theme/_layout.scss +41 -77
- package/src/theme/_mobile-sticky-menu.scss +92 -0
- package/src/theme/_search-page.scss +250 -0
- package/src/theme/_typo-custom.scss +24 -8
- package/src/theme/_variables.scss +40 -4
- package/src/theme/_widgets.scss +6 -17
- package/src/theme/blocks/_accordion.scss +11 -4
- package/src/theme/blocks/_form.scss +350 -0
- package/src/theme/blocks/_grid.scss +10 -77
- package/src/theme/blocks/_highlight.scss +10 -7
- package/src/theme/blocks/_image.scss +99 -184
- package/src/theme/blocks/_listing.scss +61 -128
- package/src/theme/blocks/_maps.scss +60 -34
- 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/_toc.scss +2 -1
- package/src/theme/card.scss +136 -69
- package/src/theme/main.scss +4 -0
- package/src/theme/notfound.scss +2 -0
- package/src/theme/person.scss +7 -1
- package/src/theme/sticky-menu.scss +7 -5
- package/src/transforms/to6.ts +5 -49
- package/src/transforms/to8.test.js +201 -0
- package/src/transforms/to8.ts +109 -0
- package/src/types.d.ts +1 -0
- package/vitest.config.mjs +28 -3
- 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 -167
- package/src/components/Widgets/Size.tsx +0 -78
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { styleDefinitionsEnhancer } from './styleDefinitions';
|
|
2
|
+
import config from '@plone/volto/registry';
|
|
3
|
+
|
|
4
|
+
describe('styleDefinitionsEnhancer', () => {
|
|
5
|
+
it('should enhance style definitions correctly', () => {
|
|
6
|
+
const data = {
|
|
7
|
+
styles: {
|
|
8
|
+
backgroundColor: 'red',
|
|
9
|
+
textColor: 'blue',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
config.registerUtility({
|
|
14
|
+
type: 'styleFieldDefinition',
|
|
15
|
+
name: 'backgroundColor',
|
|
16
|
+
method: () => {
|
|
17
|
+
return [
|
|
18
|
+
{ name: 'red', label: 'Red', style: { '--bg-color': 'red' } },
|
|
19
|
+
{ name: 'green', label: 'Green', style: { '--bg-color': 'green' } },
|
|
20
|
+
];
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const container = {};
|
|
25
|
+
|
|
26
|
+
const result = styleDefinitionsEnhancer({ data, container });
|
|
27
|
+
|
|
28
|
+
expect(result).toEqual({ '--bg-color': 'red' });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
import { findStyleByName } from '@plone/volto/helpers/Blocks/Blocks';
|
|
3
|
+
import isEmpty from 'lodash/isEmpty';
|
|
4
|
+
|
|
5
|
+
export function blockThemesEnhancer({ data, container }) {
|
|
6
|
+
if (!data['@type']) return {};
|
|
7
|
+
const blockConfig = config.blocks.blocksConfig[data['@type']];
|
|
8
|
+
if (!blockConfig) return {};
|
|
9
|
+
const blockStyleDefinitions =
|
|
10
|
+
// We look up for the blockThemes in the block's config, then in the global config
|
|
11
|
+
// We keep `colors` for BBB, but `themes` should be used
|
|
12
|
+
blockConfig.themes || blockConfig.colors || config.blocks.themes || [];
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
!isEmpty(container) &&
|
|
16
|
+
container.theme &&
|
|
17
|
+
(!data.theme || data.theme === 'default')
|
|
18
|
+
) {
|
|
19
|
+
return findStyleByName(blockStyleDefinitions, container.theme);
|
|
20
|
+
}
|
|
21
|
+
if (data.theme) {
|
|
22
|
+
return data.theme ? findStyleByName(blockStyleDefinitions, data.theme) : {};
|
|
23
|
+
} else {
|
|
24
|
+
// No theme, return default color
|
|
25
|
+
return findStyleByName(config.blocks.themes, 'default');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function styleDefinitionsEnhancer({ data, container }) {
|
|
30
|
+
let resultantStyles = {};
|
|
31
|
+
Object.keys(data.styles || {}).forEach((fieldName) => {
|
|
32
|
+
const styleFieldEnhancer = config.getUtility({
|
|
33
|
+
type: 'styleFieldDefinition',
|
|
34
|
+
name: fieldName,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (styleFieldEnhancer.method) {
|
|
38
|
+
resultantStyles = {
|
|
39
|
+
...resultantStyles,
|
|
40
|
+
...findStyleByName(
|
|
41
|
+
styleFieldEnhancer.method({ data, container }),
|
|
42
|
+
data.styles[fieldName],
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return resultantStyles;
|
|
49
|
+
}
|
|
@@ -6,6 +6,7 @@ type FormState = {
|
|
|
6
6
|
content: {
|
|
7
7
|
data: Content;
|
|
8
8
|
};
|
|
9
|
+
errorContext: Content;
|
|
9
10
|
form: {
|
|
10
11
|
global: Content;
|
|
11
12
|
};
|
|
@@ -16,11 +17,15 @@ export function useLiveData<T>(
|
|
|
16
17
|
behavior: string | undefined,
|
|
17
18
|
field: string,
|
|
18
19
|
) {
|
|
20
|
+
const errorContext = useSelector((state: FormState) => state.errorContext);
|
|
21
|
+
const context = content ?? errorContext;
|
|
22
|
+
|
|
19
23
|
const location = useLocation();
|
|
20
24
|
const addMode = location?.pathname?.endsWith('/add');
|
|
25
|
+
|
|
21
26
|
const current = behavior
|
|
22
|
-
? (
|
|
23
|
-
: (
|
|
27
|
+
? (context?.['@components']?.inherit?.[behavior]?.data?.[field] as T)
|
|
28
|
+
: (context[field] as T);
|
|
24
29
|
|
|
25
30
|
const formData = useSelector<FormState, T>(
|
|
26
31
|
(state) => state.form.global?.[field],
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Container } from '@plone/components';
|
|
|
6
6
|
import EventView from './components/Theme/EventView';
|
|
7
7
|
|
|
8
8
|
import { migrateToVLT6ColorAndWidthModel } from './transforms/to6';
|
|
9
|
+
import { migrateToVLT8FloatingBlocks } from './transforms/to8';
|
|
9
10
|
|
|
10
11
|
import installSettings from './config/settings';
|
|
11
12
|
import installBlocks from './config/blocks';
|
|
@@ -14,6 +15,8 @@ import installWidgets from './config/widgets';
|
|
|
14
15
|
import installSlots from './config/slots';
|
|
15
16
|
import installSummary from './config/summary';
|
|
16
17
|
|
|
18
|
+
import reducers from './reducers';
|
|
19
|
+
|
|
17
20
|
import '@plone/components/dist/basic.css';
|
|
18
21
|
|
|
19
22
|
import type {
|
|
@@ -96,8 +99,20 @@ const applyConfig = (config: ConfigType) => {
|
|
|
96
99
|
method: migrateToVLT6ColorAndWidthModel,
|
|
97
100
|
});
|
|
98
101
|
|
|
102
|
+
config.registerUtility({
|
|
103
|
+
name: 'migrateToVLT8FloatingBlocks',
|
|
104
|
+
type: 'transform',
|
|
105
|
+
dependencies: { reducer: 'content' },
|
|
106
|
+
method: migrateToVLT8FloatingBlocks,
|
|
107
|
+
});
|
|
108
|
+
|
|
99
109
|
config.views.contentTypesViews.Event = EventView;
|
|
100
110
|
|
|
111
|
+
config.addonReducers = {
|
|
112
|
+
...config.addonReducers,
|
|
113
|
+
...reducers,
|
|
114
|
+
};
|
|
115
|
+
|
|
101
116
|
return config;
|
|
102
117
|
};
|
|
103
118
|
|
|
@@ -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
|
+
});
|
|
@@ -165,7 +165,10 @@ export const CustomImage: Story = {
|
|
|
165
165
|
render: (args) => (
|
|
166
166
|
<div
|
|
167
167
|
className="card-listing"
|
|
168
|
-
style={{
|
|
168
|
+
style={{
|
|
169
|
+
width: 'var(--default-container-width)',
|
|
170
|
+
'--theme-high-contrast-color': '#ecebeb',
|
|
171
|
+
}}
|
|
169
172
|
>
|
|
170
173
|
<Card href={args.href}>
|
|
171
174
|
<Card.Image>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
-
import { render
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
4
|
import Card from './Card';
|
|
5
5
|
|
|
6
6
|
vi.mock(
|
|
@@ -43,10 +43,16 @@ vi.mock(
|
|
|
43
43
|
|
|
44
44
|
type SummaryProps = {
|
|
45
45
|
a11yLabelId?: string;
|
|
46
|
+
LinkToItem?: React.ElementType;
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
const SummaryContent = ({
|
|
49
|
-
|
|
49
|
+
const SummaryContent = ({
|
|
50
|
+
a11yLabelId,
|
|
51
|
+
LinkToItem = React.Fragment,
|
|
52
|
+
}: SummaryProps) => (
|
|
53
|
+
<h3 id={a11yLabelId}>
|
|
54
|
+
<LinkToItem>Card title</LinkToItem>
|
|
55
|
+
</h3>
|
|
50
56
|
);
|
|
51
57
|
|
|
52
58
|
const BodyContent = () => <div>Body content</div>;
|
|
@@ -64,26 +70,20 @@ describe('Card', () => {
|
|
|
64
70
|
|
|
65
71
|
it('is interactive when an href is provided', () => {
|
|
66
72
|
const { container } = renderCard({ href: '/target', className: 'custom' });
|
|
67
|
-
const card = container.querySelector('.card') as HTMLElement;
|
|
68
|
-
|
|
69
|
-
expect(card).toHaveAttribute('role', 'link');
|
|
70
|
-
expect(card).toHaveAttribute('tabindex', '0');
|
|
71
73
|
|
|
72
74
|
const anchor = container.querySelector('a');
|
|
73
75
|
expect(anchor).not.toBeNull();
|
|
74
76
|
expect(anchor).toHaveAttribute('href', '/target');
|
|
77
|
+
expect(anchor).toHaveClass('card-primary-link');
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
it('is interactive when an item is provided', () => {
|
|
78
81
|
const { container } = renderCard({ item: { '@id': '/item-target' } });
|
|
79
|
-
const card = container.querySelector('.card') as HTMLElement;
|
|
80
|
-
|
|
81
|
-
expect(card).toHaveAttribute('role', 'link');
|
|
82
|
-
expect(card).toHaveAttribute('tabindex', '0');
|
|
83
82
|
|
|
84
83
|
const anchor = container.querySelector('a');
|
|
85
84
|
expect(anchor).not.toBeNull();
|
|
86
85
|
expect(anchor).toHaveAttribute('href', '/item-target');
|
|
86
|
+
expect(anchor).toHaveClass('card-primary-link');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
it('is not interactive when neither href nor item is provided', () => {
|
|
@@ -112,26 +112,4 @@ describe('Card', () => {
|
|
|
112
112
|
expect(card).not.toHaveAttribute('tabindex');
|
|
113
113
|
expect(container.querySelector('a')).toBeNull();
|
|
114
114
|
});
|
|
115
|
-
|
|
116
|
-
it('triggers navigation handlers when interactive', () => {
|
|
117
|
-
const clickSpy = vi
|
|
118
|
-
.spyOn(HTMLAnchorElement.prototype, 'click')
|
|
119
|
-
.mockImplementation(() => {});
|
|
120
|
-
const selectionSpy = vi
|
|
121
|
-
.spyOn(window, 'getSelection')
|
|
122
|
-
.mockReturnValue({ toString: () => '' } as unknown as Selection);
|
|
123
|
-
|
|
124
|
-
const { container } = renderCard({ href: '/target' });
|
|
125
|
-
const card = container.querySelector('.card') as HTMLElement;
|
|
126
|
-
|
|
127
|
-
fireEvent.click(card);
|
|
128
|
-
fireEvent.keyDown(card, { key: 'Enter' });
|
|
129
|
-
fireEvent.keyDown(card, { key: ' ' });
|
|
130
|
-
fireEvent.keyDown(card, { key: 'Escape' });
|
|
131
|
-
|
|
132
|
-
expect(clickSpy).toHaveBeenCalledTimes(3);
|
|
133
|
-
|
|
134
|
-
clickSpy.mockRestore();
|
|
135
|
-
selectionSpy.mockRestore();
|
|
136
|
-
});
|
|
137
115
|
});
|
|
@@ -52,49 +52,32 @@ const Card = (props: CardProps) => {
|
|
|
52
52
|
const { className, openLinkInNewTab } = props;
|
|
53
53
|
|
|
54
54
|
const a11yLabelId = React.useId();
|
|
55
|
-
const linkRef = React.useRef<HTMLAnchorElement>(null);
|
|
56
|
-
|
|
57
|
-
const triggerNavigation = () => {
|
|
58
|
-
// Only navigate if there is *no* text selection
|
|
59
|
-
const hasSelection = !!window.getSelection()?.toString();
|
|
60
|
-
if (!hasSelection) {
|
|
61
|
-
linkRef.current?.click();
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
55
|
const isInteractive = !!props.href || !!props.item;
|
|
66
56
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
57
|
+
const LinkToItem = React.useCallback(
|
|
58
|
+
({ children }: { children: React.ReactNode }) => {
|
|
59
|
+
return (
|
|
60
|
+
<ConditionalLink
|
|
61
|
+
className="card-primary-link"
|
|
62
|
+
condition={isInteractive}
|
|
63
|
+
href={href}
|
|
64
|
+
item={item}
|
|
65
|
+
openLinkInNewTab={openLinkInNewTab}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</ConditionalLink>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
[href, item, isInteractive, openLinkInNewTab],
|
|
72
|
+
);
|
|
78
73
|
|
|
79
74
|
return (
|
|
80
|
-
<div
|
|
81
|
-
className={cx('card', className)}
|
|
82
|
-
onClick={isInteractive ? onClick : undefined}
|
|
83
|
-
onKeyDown={isInteractive ? onKeyDown : undefined}
|
|
84
|
-
role={isInteractive ? 'link' : undefined}
|
|
85
|
-
tabIndex={isInteractive ? 0 : undefined}
|
|
86
|
-
>
|
|
87
|
-
{/* @ts-expect-error since this has no children, should fail */}
|
|
88
|
-
<ConditionalLink
|
|
89
|
-
aria-labelledby={a11yLabelId}
|
|
90
|
-
condition={isInteractive}
|
|
91
|
-
href={href}
|
|
92
|
-
item={item}
|
|
93
|
-
openLinkInNewTab={openLinkInNewTab}
|
|
94
|
-
ref={linkRef}
|
|
95
|
-
/>
|
|
75
|
+
<div className={cx('card', className)}>
|
|
96
76
|
<div className="card-inner">
|
|
97
|
-
{childrenWithProps(props.children, {
|
|
77
|
+
{childrenWithProps(props.children, {
|
|
78
|
+
a11yLabelId,
|
|
79
|
+
LinkToItem,
|
|
80
|
+
})}
|
|
98
81
|
</div>
|
|
99
82
|
</div>
|
|
100
83
|
);
|
|
@@ -111,10 +94,12 @@ type CardImageProps = {
|
|
|
111
94
|
imageComponent?: React.ComponentType<any>;
|
|
112
95
|
children?: React.ReactNode;
|
|
113
96
|
showPlaceholderImage?: boolean;
|
|
97
|
+
sizes?: string;
|
|
114
98
|
};
|
|
115
99
|
|
|
116
100
|
const CardImage = (props: CardImageProps) => {
|
|
117
|
-
const { src, item, image, imageComponent, showPlaceholderImage } =
|
|
101
|
+
const { src, item, image, imageComponent, showPlaceholderImage, sizes } =
|
|
102
|
+
props;
|
|
118
103
|
const Image = imageComponent || DefaultImage;
|
|
119
104
|
|
|
120
105
|
return (
|
|
@@ -132,6 +117,7 @@ const CardImage = (props: CardImageProps) => {
|
|
|
132
117
|
alt=""
|
|
133
118
|
loading="lazy"
|
|
134
119
|
responsive={true}
|
|
120
|
+
sizes={sizes}
|
|
135
121
|
/>
|
|
136
122
|
)
|
|
137
123
|
) : (
|
|
@@ -144,14 +130,21 @@ const CardImage = (props: CardImageProps) => {
|
|
|
144
130
|
type CardSummaryProps = {
|
|
145
131
|
/** The ID of the element that labels the card. */
|
|
146
132
|
a11yLabelId?: string;
|
|
133
|
+
LinkToItem?: React.ElementType;
|
|
147
134
|
children?: React.ReactNode;
|
|
148
135
|
};
|
|
149
136
|
|
|
150
|
-
const CardSummary = (props: CardSummaryProps) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
137
|
+
const CardSummary = (props: CardSummaryProps) => {
|
|
138
|
+
const { a11yLabelId, LinkToItem } = props;
|
|
139
|
+
return (
|
|
140
|
+
<div className="card-summary">
|
|
141
|
+
{childrenWithProps(props.children, {
|
|
142
|
+
a11yLabelId,
|
|
143
|
+
LinkToItem,
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
155
148
|
|
|
156
149
|
const CardActions = (props: any) => (
|
|
157
150
|
<div className="actions-wrapper">{props.children}</div>
|
|
@@ -6,6 +6,58 @@ type IconLinkListProps = {
|
|
|
6
6
|
iconLinks: Array<iconLink>;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
export const IconLinkListTemplate = ({ item }) => {
|
|
10
|
+
const itemInfo: {
|
|
11
|
+
title: string;
|
|
12
|
+
hrefTitle: string;
|
|
13
|
+
href: string;
|
|
14
|
+
itemHref: string;
|
|
15
|
+
src: string;
|
|
16
|
+
srcAlt: string;
|
|
17
|
+
} = {
|
|
18
|
+
title: '',
|
|
19
|
+
hrefTitle: '',
|
|
20
|
+
href: '',
|
|
21
|
+
itemHref: '',
|
|
22
|
+
src: '',
|
|
23
|
+
srcAlt: '',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// If the item has title, always set it
|
|
27
|
+
itemInfo.title = item ? item?.title || item?.href?.[0]?.['title'] || '' : '';
|
|
28
|
+
if (item?.href?.length > 0) {
|
|
29
|
+
itemInfo.title = item.title || item.href[0]['title'];
|
|
30
|
+
itemInfo.href = flattenToAppURL(item.href[0]['@id']);
|
|
31
|
+
}
|
|
32
|
+
if (item?.icon && item.icon[0]?.image_scales) {
|
|
33
|
+
itemInfo.itemHref = item.icon[0]['@id'];
|
|
34
|
+
itemInfo.srcAlt = item['alt'];
|
|
35
|
+
itemInfo.src = `${flattenToAppURL(itemInfo.itemHref)}/${item.icon[0].image_scales[item.icon[0].image_field][0].download}`;
|
|
36
|
+
} else if (item?.icon && item.icon[0]) {
|
|
37
|
+
itemInfo.itemHref = item.icon[0]['@id'];
|
|
38
|
+
itemInfo.srcAlt = item['alt'];
|
|
39
|
+
itemInfo.src = `${flattenToAppURL(itemInfo.itemHref)}/@@images/image`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!itemInfo.src) return null;
|
|
43
|
+
return (
|
|
44
|
+
<li className="item" key={item['@id']}>
|
|
45
|
+
{/* @ts-ignore */}
|
|
46
|
+
<ConditionalLink
|
|
47
|
+
condition={itemInfo.href}
|
|
48
|
+
to={itemInfo.href}
|
|
49
|
+
title={itemInfo.hrefTitle || itemInfo.srcAlt}
|
|
50
|
+
openLinkInNewTab={item.openInNewTab}
|
|
51
|
+
>
|
|
52
|
+
<div className="image-wrapper">
|
|
53
|
+
<img src={itemInfo.src} alt={itemInfo.srcAlt || ''} />
|
|
54
|
+
</div>
|
|
55
|
+
<span className="title">{itemInfo.title}</span>
|
|
56
|
+
</ConditionalLink>
|
|
57
|
+
</li>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
9
61
|
const IconLinkList = (props: IconLinkListProps) => {
|
|
10
62
|
const { iconLinks } = props;
|
|
11
63
|
|
|
@@ -13,58 +65,7 @@ const IconLinkList = (props: IconLinkListProps) => {
|
|
|
13
65
|
<ul>
|
|
14
66
|
{iconLinks && Array.isArray(iconLinks)
|
|
15
67
|
? iconLinks.map((item) => {
|
|
16
|
-
|
|
17
|
-
title: string;
|
|
18
|
-
hrefTitle: string;
|
|
19
|
-
href: string;
|
|
20
|
-
itemHref: string;
|
|
21
|
-
src: string;
|
|
22
|
-
srcAlt: string;
|
|
23
|
-
} = {
|
|
24
|
-
title: '',
|
|
25
|
-
hrefTitle: '',
|
|
26
|
-
href: '',
|
|
27
|
-
itemHref: '',
|
|
28
|
-
src: '',
|
|
29
|
-
srcAlt: '',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// If the item has title, always set it
|
|
33
|
-
itemInfo.title = item
|
|
34
|
-
? item?.title || item?.href?.[0]?.['title'] || ''
|
|
35
|
-
: '';
|
|
36
|
-
if (item?.href?.length > 0) {
|
|
37
|
-
itemInfo.title = item.title || item.href[0]['title'];
|
|
38
|
-
itemInfo.href = flattenToAppURL(item.href[0]['@id']);
|
|
39
|
-
}
|
|
40
|
-
if (item?.icon && item.icon[0]?.image_scales) {
|
|
41
|
-
itemInfo.itemHref = item.icon[0]['@id'];
|
|
42
|
-
itemInfo.srcAlt = item['alt'];
|
|
43
|
-
itemInfo.src = `${flattenToAppURL(itemInfo.itemHref)}/${item.icon[0].image_scales[item.icon[0].image_field][0].download}`;
|
|
44
|
-
} else if (item?.icon && item.icon[0]) {
|
|
45
|
-
itemInfo.itemHref = item.icon[0]['@id'];
|
|
46
|
-
itemInfo.srcAlt = item['alt'];
|
|
47
|
-
itemInfo.src = `${flattenToAppURL(itemInfo.itemHref)}/@@images/image`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!itemInfo.src) return null;
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<li className="item" key={item['@id']}>
|
|
54
|
-
{/* @ts-ignore */}
|
|
55
|
-
<ConditionalLink
|
|
56
|
-
condition={itemInfo.href}
|
|
57
|
-
to={itemInfo.href}
|
|
58
|
-
title={itemInfo.hrefTitle || itemInfo.srcAlt}
|
|
59
|
-
openLinkInNewTab={item.openInNewTab}
|
|
60
|
-
>
|
|
61
|
-
<div className="image-wrapper">
|
|
62
|
-
<img src={itemInfo.src} alt={itemInfo.srcAlt || ''} />
|
|
63
|
-
</div>
|
|
64
|
-
<span>{itemInfo.title}</span>
|
|
65
|
-
</ConditionalLink>
|
|
66
|
-
</li>
|
|
67
|
-
);
|
|
68
|
+
return <IconLinkListTemplate item={item} key={item['@id']} />;
|
|
68
69
|
})
|
|
69
70
|
: null}
|
|
70
71
|
</ul>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useSelector } from 'react-redux';
|
|
2
|
+
import { useLocation, useHistory } from 'react-router-dom';
|
|
3
|
+
import type { ObjectBrowserItem, GetSiteResponse } from '@plone/types';
|
|
4
|
+
import linkSVG from '@plone/volto/icons/link.svg';
|
|
5
|
+
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
6
|
+
import { Button } from '@plone/components';
|
|
7
|
+
import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
8
|
+
|
|
9
|
+
type FormState = {
|
|
10
|
+
site: { data: GetSiteResponse };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const useLinkIconNavigation = (item?: Partial<ObjectBrowserItem>) => {
|
|
14
|
+
const location = useLocation();
|
|
15
|
+
const history = useHistory();
|
|
16
|
+
|
|
17
|
+
const handleLinkIconClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
|
|
21
|
+
const targetUrl = item?.['@id'];
|
|
22
|
+
if (targetUrl) {
|
|
23
|
+
const flattenedTargetUrl = flattenToAppURL(targetUrl);
|
|
24
|
+
const searchParams = new URLSearchParams();
|
|
25
|
+
searchParams.set('return_to', location.pathname);
|
|
26
|
+
|
|
27
|
+
history.push({
|
|
28
|
+
pathname: flattenedTargetUrl,
|
|
29
|
+
search: searchParams.toString(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return handleLinkIconClick;
|
|
34
|
+
};
|
|
35
|
+
const LinkIconButton = ({ item }: { item?: Partial<ObjectBrowserItem> }) => {
|
|
36
|
+
const site = useSelector<FormState, GetSiteResponse>(
|
|
37
|
+
(state) => state.site?.data,
|
|
38
|
+
);
|
|
39
|
+
const hideProfileLinks = site?.['kitconcept.disable_profile_links'];
|
|
40
|
+
const handleLinkIconClick = useLinkIconNavigation(item);
|
|
41
|
+
return (
|
|
42
|
+
hideProfileLinks && (
|
|
43
|
+
<div className="card-link-icon">
|
|
44
|
+
<Button aria-label="link" onClick={handleLinkIconClick}>
|
|
45
|
+
<Icon name={linkSVG} size="33px" />
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default LinkIconButton;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GET_CONTENT } from '@plone/volto/constants/ActionTypes';
|
|
2
|
+
|
|
3
|
+
const initialState = {};
|
|
4
|
+
|
|
5
|
+
export default function errorContext(state = initialState, action: any = {}) {
|
|
6
|
+
switch (action.type) {
|
|
7
|
+
case `${GET_CONTENT}_PENDING`:
|
|
8
|
+
return {};
|
|
9
|
+
case `${GET_CONTENT}_FAIL`:
|
|
10
|
+
return action.error.response?.body ?? {};
|
|
11
|
+
default:
|
|
12
|
+
return state;
|
|
13
|
+
}
|
|
14
|
+
}
|