@eeacms/volto-cca-policy 0.3.124 → 0.3.125

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.
@@ -0,0 +1,60 @@
1
+ # How Volto Discovers Links in Blocks for Link Integrity
2
+
3
+ When a content item is saved in Plone, the system needs to know which other internal items it links to, so it can maintain the `zc.relation` catalog (specifically using the `isReferencing` relationship).
4
+
5
+ For traditional Plone, this meant parsing HTML in `RichText` fields. For Volto, the content is stored as JSON blocks, requiring a recursive discovery mechanism.
6
+
7
+ ## Sub-block Storage and Discovery
8
+
9
+ Sub-blocks are stored in the JSON data of a parent block using one of two primary patterns. The Plone backend uses a recursive generator called `visit_blocks` (defined in `plone.restapi.blocks`) to walk this tree.
10
+
11
+ ### 1. Dictionary Mapping (Key-Value)
12
+ This is the standard Volto pattern for container-like blocks.
13
+ - **Storage Pattern**: A dictionary where keys are UUIDs and values are the sub-block data.
14
+ - **Common Keys**: `blocks` or `data["blocks"]`.
15
+ - **Discovery**: `plone.restapi.blocks.NestedBlocksVisitor` is registered for these keys. It yields each value in the dictionary.
16
+
17
+ ### 2. Sequential Lists
18
+ This pattern is used by layout-oriented blocks where the order is strictly positional.
19
+ - **Storage Pattern**: A simple list of block data objects.
20
+ - **Core Volto Keys**: `columns`, `slides`, `hrefList`.
21
+ - **Discovery**: `plone.volto.transforms.NestedBlocksVisitor` handles these keys. It iterates through the list and yields each block.
22
+
23
+ ### 3. Recursive Discovery Logic
24
+ The `visit_blocks` function uses `IBlockVisitor` subscribers to find children. When a visitor yields a sub-block, `visit_blocks` immediately recurses into that sub-block. This ensures that a block tree of any depth (e.g., a Grid containing Columns containing Teasers) is fully flattened during the discovery phase.
25
+
26
+ ## Internal Link Extraction
27
+
28
+ Once a block is discovered, the `BlocksRetriever` (in `plone.restapi.blocks_linkintegrity`) uses `IBlockFieldLinkIntegrityRetriever` subscribers to find internal links within that specific block's fields.
29
+
30
+ ### Automatic Link Detection (The Generic Retriever)
31
+ The `GenericBlockLinksRetriever` provides a "zero-configuration" discovery for most custom blocks. It automatically scans the following fields if they contain the `resolveuid/` pattern:
32
+ - `url`
33
+ - `href`
34
+ - `preview_image`
35
+
36
+ **Note**: If a block uses custom field names for links (e.g., `source`, `target`, `link_to`), they will **NOT** be caught by the generic retriever unless they are specifically registered.
37
+
38
+ ### Complex Data Extraction
39
+ Some blocks store links deep within complex JSON structures rather than simple top-level strings. These require specialized retrievers:
40
+
41
+ 1. **Slate Blocks (Rich Text)**:
42
+ - Uses `SlateBlockLinksRetriever`.
43
+ - Recursively walks the Slate node tree.
44
+ - Extracts UIDs from `data.link.internal.internal_link[0]["@id"]` and `data.url`.
45
+
46
+ 2. **DraftJS Blocks (Legacy Text)**:
47
+ - Uses `TextBlockLinksRetriever`.
48
+ - Parses the `entityMap` for `LINK` entities.
49
+ - Extracts data from `url` and `href` keys within the entity data.
50
+
51
+ ## Redundancy and Reliability
52
+ To ensure maximum reliability, `plone.volto` includes a `NestedBlockLinkRetriever` (registered with `block_type = None`). This serves as a fail-safe that manually walks the `columns`, `hrefList`, and `slides` keys to trigger link extraction, providing a second layer of defense for nested layouts.
53
+
54
+ ## Summary for Developers
55
+ To ensure a custom block is correctly tracked by the link integrity system:
56
+
57
+ 1. **Field Naming**: Prefer naming your link fields `url` or `href` to benefit from the generic retriever.
58
+ 2. **Nesting**: If your block contains sub-blocks, store them in a dictionary under the key `blocks`.
59
+ 3. **Link Format**: Always store internal links using the `resolveuid/<UID>` format.
60
+ 4. **Custom Retrievers**: If you must use unique field names or storage patterns, register a custom `IBlockFieldLinkIntegrityRetriever` adapter for your block's `@type` in the backend.
@@ -0,0 +1,267 @@
1
+ # Test Fix Specification for volto-cca-policy
2
+
3
+ > Generated: 2026-05-14
4
+ > Branch: `link-integrity-workflow`
5
+ > Machine timezone: EEST (UTC+3)
6
+
7
+ ## Current State
8
+
9
+ ```
10
+ Test Suites: 4 failed, 57 passed, 61 total
11
+ Tests: 7 failed, 115 passed, 122 total
12
+ Snapshots: 6 failed, 12 passed, 18 total
13
+ ```
14
+
15
+ Four test suites fail for three distinct root causes:
16
+
17
+ | # | Test File | Failure Type | Root Cause |
18
+ |---|-----------|-------------|------------|
19
+ | 1 | `Spotlight.test.jsx` | Suite won't run | `node:crypto` module resolution (uuid@10 + Jest 26) |
20
+ | 2 | `GeolocationWidget.test.jsx` | Assertion error | Mock path mismatch → real OpenLayers loads in Jest |
21
+ | 3 | `EventView.test.jsx` | 3 snapshot mismatches | Timezone-dependent date formatting |
22
+ | 4 | `CcaEventView.test.jsx` | 3 snapshot mismatches | Timezone-dependent date formatting |
23
+
24
+ ---
25
+
26
+ ## Problem 1: `node:crypto` — Spotlight.test.jsx
27
+
28
+ ### Symptom
29
+
30
+ ```
31
+ ENOENT: no such file or directory, open 'node:crypto'
32
+ at Runtime.readFile (node_modules/jest-runtime/build/index.js:1987:21)
33
+ at Object.<anonymous> (node_modules/uuid/dist/rng.js:7:42)
34
+ ```
35
+
36
+ ### Root Cause
37
+
38
+ The `uuid` package (v10.0.0) uses `require('node:crypto')` — the Node.js built-in module protocol prefix (`node:`). Jest 26.6.3 does not understand this protocol and tries to resolve `node:crypto` as a filesystem path, which fails.
39
+
40
+ `Spotlight.test.jsx` doesn't import `uuid` directly. It's a transitive dependency pulled in through Volto core or one of its addons when the Spotlight component tree is evaluated.
41
+
42
+ ### Proposed Fix
43
+
44
+ Add a `moduleNameMapper` entry in `jest-addon.config.js` to redirect `node:crypto` to the real Node.js `crypto` module:
45
+
46
+ **File: `jest-addon.config.js`**
47
+
48
+ In the `moduleNameMapper` section, add:
49
+
50
+ ```js
51
+ '^node:crypto$': 'crypto',
52
+ ```
53
+
54
+ This tells Jest to resolve `node:crypto` to the standard `crypto` module, which Node.js provides natively and Jest can handle.
55
+
56
+ **Why this approach:**
57
+ - Minimal: one line in the config, no new files needed.
58
+ - Follows the established pattern used across the addon ecosystem (moduleNameMapper entries in jest-addon.config.js).
59
+ - Does NOT require creating a mock file (the AI agent's `crypto-mock.js` approach).
60
+ - The real `crypto` module works fine in Jest's Node.js environment — it just needs the `node:` prefix stripped.
61
+
62
+ ---
63
+
64
+ ## Problem 2: Mock path mismatch — GeolocationWidget.test.jsx
65
+
66
+ ### Symptom
67
+
68
+ ```
69
+ Invalid lib or bundle name ol,olCondition,olControl,olCoordinate,olEvents,olExtent,olFormat,olGeom,olInteraction,olLayer,olLoadingstrategy,olOverlay,olProj,olRender,olSource,olStyle,olTilegrid
70
+ at flattenLazyBundle (Loadable/Loadable.js:30:11)
71
+ ```
72
+
73
+ ### Root Cause
74
+
75
+ Two layers of the problem:
76
+
77
+ **Layer 1 — Webpack vs Jest module resolution mismatch:**
78
+
79
+ Volto's webpack config uses `RelativeResolverPlugin` (`webpack-plugins/webpack-relative-resolver.js`) which rewrites relative imports into absolute addon-scoped paths at build time. For example, in production:
80
+
81
+ ```
82
+ import MapContainer from './GeolocationWidgetMapContainer'
83
+ ```
84
+
85
+ gets resolved by webpack to:
86
+
87
+ ```
88
+ @eeacms/volto-cca-policy/components/theme/Widgets/GeolocationWidgetMapContainer
89
+ ```
90
+
91
+ Jest does **not** use webpack. It resolves `./GeolocationWidgetMapContainer` as a relative filesystem path. So the two environments resolve the same import line to **different module identifiers**.
92
+
93
+ **Layer 2 — The test mocks the absolute path, but Jest loads the relative path:**
94
+
95
+ ```js
96
+ // In the test — mocks the absolute (webpack-resolved) path:
97
+ jest.mock(
98
+ '@eeacms/volto-cca-policy/components/theme/Widgets/GeolocationWidgetMapContainer',
99
+ );
100
+
101
+ // But GeolocationWidget.jsx imports it relatively:
102
+ import MapContainer from './GeolocationWidgetMapContainer';
103
+ ```
104
+
105
+ In Jest, these are two different modules. The mock never applies. The real `GeolocationWidgetMapContainer` loads, which uses the `withOpenLayers` HOC from `volto-openlayers-map`. That HOC calls `useLazyLibs()` to dynamically load OpenLayers bundles, which crashes in Jest's environment.
106
+
107
+ ### Proposed Fix
108
+
109
+ Two options. **Option A is recommended** because it fixes the source code to match the project convention (the rest of the codebase uses `@eeacms/volto-cca-policy/...` absolute imports — the relative import in `GeolocationWidget.jsx` is the outlier).
110
+
111
+ ---
112
+
113
+ #### Option A: Change the source code to use the absolute import (recommended)
114
+
115
+ **File: `src/components/theme/Widgets/GeolocationWidget.jsx`**
116
+
117
+ Replace:
118
+
119
+ ```js
120
+ import MapContainer from './GeolocationWidgetMapContainer';
121
+ ```
122
+
123
+ With:
124
+
125
+ ```js
126
+ import MapContainer from '@eeacms/volto-cca-policy/components/theme/Widgets/GeolocationWidgetMapContainer';
127
+ ```
128
+
129
+ This makes the import consistent with the rest of the codebase (20+ files in this addon use `@eeacms/volto-cca-policy/...` imports). The existing test mock path (`@eeacms/volto-cca-policy/components/theme/Widgets/GeolocationWidgetMapContainer`) will now match in **both** webpack and Jest environments. No test file changes needed.
130
+
131
+ ---
132
+
133
+ #### Option B: Mock both paths in the test file
134
+
135
+ If changing the source code is not desired, mock **both** the relative and absolute paths in the test:
136
+
137
+ **File: `src/components/theme/Widgets/GeolocationWidget.test.jsx`**
138
+
139
+ Replace:
140
+
141
+ ```js
142
+ jest.mock(
143
+ '@eeacms/volto-cca-policy/components/theme/Widgets/GeolocationWidgetMapContainer',
144
+ );
145
+ ```
146
+
147
+ With:
148
+
149
+ ```js
150
+ const MapContainerMock = () => <div id="map-container-mock" />;
151
+
152
+ // Mock the relative path (what Jest resolves):
153
+ jest.mock('./GeolocationWidgetMapContainer', () => MapContainerMock);
154
+
155
+ // Mock the absolute path (what webpack resolves — belt and suspenders):
156
+ jest.mock(
157
+ '@eeacms/volto-cca-policy/components/theme/Widgets/GeolocationWidgetMapContainer',
158
+ () => MapContainerMock,
159
+ );
160
+ ```
161
+
162
+ **Why Option A is preferred:**
163
+ - One-line source code change, zero test changes.
164
+ - The relative import `./GeolocationWidgetMapContainer` is the **only** relative import to a sibling file within this addon's component tree. Every other internal import in `volto-cca-policy` uses the `@eeacms/volto-cca-policy/...` convention.
165
+ - Fixes the root cause (import inconsistency) rather than working around it in every test file.
166
+ - The existing test mock already targets the correct absolute path — it just never matched because of the relative import in the source.
167
+
168
+ ---
169
+
170
+ ## Problem 3 & 4: Timezone-dependent snapshots — EventView.test.jsx, CcaEventView.test.jsx
171
+
172
+ ### Symptom
173
+
174
+ ```
175
+ - Snapshot - 2
176
+ + Received + 2
177
+
178
+ <span className="start-time">
179
+ - 3:20 PM
180
+ + 6:20 PM
181
+ </span>
182
+ ```
183
+
184
+ The snapshots expect `3:20 PM` but the test produces `6:20 PM` (a 3-hour offset = UTC vs EEST).
185
+
186
+ ### Root Cause
187
+
188
+ The test data uses UTC timestamps (e.g., `'2019-06-23T15:20:00+00:00'`). The `EventDetails` component (from `@eeacms/volto-cca-policy/helpers`) formats these dates using JavaScript's built-in `Date` methods, which convert to the **local timezone**. The snapshots were recorded on a machine in UTC (or a timezone with offset 0), but the current test machine is in EEST (UTC+3).
189
+
190
+ The snapshots contain:
191
+ - `3:20 PM` for `15:20:00+00:00` — correct in UTC
192
+ - `4:20 PM` for `16:20:00+00:00` — correct in UTC
193
+
194
+ On EEST, these render as `6:20 PM` and `7:20 PM` respectively.
195
+
196
+ ### Proposed Fix
197
+
198
+ Set `TZ=UTC` for the Jest process so date formatting is deterministic and matches the snapshots. This is done via the `TZ` environment variable, which is the standard, cross-platform way to control timezone in JavaScript tests.
199
+
200
+ Two approaches (pick one):
201
+
202
+ **Approach A — Makefile target (recommended):**
203
+
204
+ Modify the `test` target in `frontend/Makefile` to set `TZ=UTC`:
205
+
206
+ ```makefile
207
+ test: ## Run Jest tests for Volto add-on
208
+ TZ=UTC RAZZLE_JEST_CONFIG=$(filter-out $@,$(MAKECMDGOALS))/jest-addon.config.js yarn test $(filter-out $@,$(MAKECMDGOALS))
209
+ ```
210
+
211
+ This is the cleanest approach because:
212
+ - It applies to ALL addon tests consistently, preventing the same issue in other addons.
213
+ - No changes needed in individual test files.
214
+ - `TZ=UTC` is the standard approach for timezone-independent tests in CI environments.
215
+
216
+ **Approach B — jest.setup.js (if Approach A is not preferred):**
217
+
218
+ Add to `jest.setup.js` (at the top, before any imports that might use Date):
219
+
220
+ ```js
221
+ // Ensure deterministic timezone for snapshot tests
222
+ process.env.TZ = 'UTC';
223
+ ```
224
+
225
+ **Why this approach:**
226
+ - `TZ=UTC` is the established convention for deterministic date formatting in tests.
227
+ - Does not modify the component code or test data.
228
+ - Does not require updating snapshots — the tests will produce the same output as the recorded snapshots.
229
+ - Solves the problem for any future tests that render dates.
230
+
231
+ ---
232
+
233
+ ## Summary of Changes
234
+
235
+ ### Option A (recommended — fix source import)
236
+
237
+ | File | Change | Lines |
238
+ |------|--------|-------|
239
+ | `jest-addon.config.js` | Add `'^node:crypto$': 'crypto'` to `moduleNameMapper` | +1 |
240
+ | `src/components/theme/Widgets/GeolocationWidget.jsx` | Change `import ... from './GeolocationWidgetMapContainer'` → `import ... from '@eeacms/volto-cca-policy/components/theme/Widgets/GeolocationWidgetMapContainer'` | 1 line |
241
+ | `frontend/Makefile` (root) | Add `TZ=UTC` to the `test` target | +1 |
242
+
243
+ ### Option B (mock both paths in test)
244
+
245
+ | File | Change | Lines |
246
+ |------|--------|-------|
247
+ | `jest-addon.config.js` | Add `'^node:crypto$': 'crypto'` to `moduleNameMapper` | +1 |
248
+ | `src/components/theme/Widgets/GeolocationWidget.test.jsx` | Mock both `./GeolocationWidgetMapContainer` and `@eeacms/.../GeolocationWidgetMapContainer` | ~6 lines |
249
+ | `frontend/Makefile` (root) | Add `TZ=UTC` to the `test` target | +1 |
250
+
251
+ **No changes needed to:**
252
+ - `jest.setup.js` — the existing setup (with `thunk` middleware) is correct.
253
+ - `EventView.test.jsx` or `CcaEventView.test.jsx` — the snapshots are correct (UTC), the timezone environment is the fix.
254
+ - Any snapshot files — once `TZ=UTC` is set, they will match.
255
+ - `GeolocationWidget.test.jsx` (Option A only) — the existing absolute-path mock already works once the source import is aligned.
256
+
257
+ ## What the Previous AI Agent Did Wrong
258
+
259
+ The previous attempt made these changes (now stashed/discarded):
260
+
261
+ 1. **`jest.setup.js` — massive rewrite:** Mocked `uuid`, `redux-mock-store`, `useLazyLibs` globally; added `lazyBundles`/`loadables` config; switched to CommonJS `require()`. This was overly invasive — it changed the global test environment for ALL 61 test suites, introducing risk of masking real issues.
262
+
263
+ 2. **`EventView.test.jsx` and `CcaEventView.test.jsx`:** Removed the `jest.mock('Loadable')` + `beforeAll(__setLoadables())` pattern. This was counterproductive — the Loadable mock is the established pattern used by 19+ test files across the addons. Removing it doesn't fix the timezone issue at all.
264
+
265
+ 3. **`GeolocationWidget.test.jsx`:** Added `thunk` middleware to the local `configureStore()` call. Unnecessary — `jest.setup.js` already configures `configureStore([thunk])` globally. Also changed the mock path to `./GeolocationWidgetMapContainer` which fixes the Jest-side resolution but doesn't address the root cause: the source code uses a relative import where the rest of the codebase uses absolute `@eeacms/...` imports, creating a webpack/Jest resolution mismatch.
266
+
267
+ 4. **Created `src/crypto-mock.js`:** An unnecessary file when `moduleNameMapper: 'node:crypto' -> 'crypto'` solves it in one line.
@@ -411,6 +411,7 @@ module.exports = {
411
411
  'config\\.[jt]sx?$',
412
412
  ],
413
413
  moduleNameMapper: {
414
+ '^node:crypto$': '<rootDir>/src/addons/volto-cca-policy/jest-node-crypto-mock.js',
414
415
  '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
415
416
  '@plone/volto/cypress': '<rootDir>/node_modules/@plone/volto/cypress',
416
417
  '@plone/volto/babel': '<rootDir>/node_modules/@plone/volto/babel',
@@ -0,0 +1,3 @@
1
+ // Mock for node:crypto protocol — redirects to the real Node.js crypto built-in
2
+ // Used by Jest 26 which doesn't understand the 'node:' protocol prefix
3
+ module.exports = require('crypto');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "0.3.124",
3
+ "version": "0.3.125",
4
4
  "description": "@eeacms/volto-cca-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -14,6 +14,7 @@ export { default as ImageWidget } from './theme/Widgets/ImageWidget';
14
14
 
15
15
  // Manage
16
16
  export { default as CreateArchivedCopyButton } from './manage/CreateArchivedCopyButton/CreateArchivedCopyButton';
17
+ export { default as WorkflowLinkIntegrityModal } from './manage/Workflow/WorkflowLinkIntegrityModal';
17
18
 
18
19
  // Views
19
20
  export { default as ArchivedVersionListing } from './theme/Views/ArchivedVersionListing';
@@ -3,6 +3,19 @@ import { render, screen, fireEvent } from '@testing-library/react';
3
3
  import ReadMoreView from './ReadMoreView';
4
4
 
5
5
  describe('ReadMoreView', () => {
6
+ /* eslint-disable no-console */
7
+ const originalError = console.error;
8
+ beforeAll(() => {
9
+ console.error = jest.fn((...args) => {
10
+ if (args[0]?.includes?.('unmountComponentAtNode')) return;
11
+ originalError(...args);
12
+ });
13
+ });
14
+ afterAll(() => {
15
+ console.error = originalError;
16
+ });
17
+ /* eslint-enable no-console */
18
+
6
19
  it('renders with default labels and toggles on click', () => {
7
20
  const data = {
8
21
  label_closed: 'Show more',
@@ -0,0 +1,192 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useSelector } from 'react-redux';
4
+ import { Link } from 'react-router-dom';
5
+ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
6
+ import { flattenToAppURL } from '@plone/volto/helpers';
7
+ import { Confirm, Dimmer, Loader, Table } from 'semantic-ui-react';
8
+
9
+ const messages = defineMessages({
10
+ confirmHeader: {
11
+ id: 'Warning: Potential broken links',
12
+ defaultMessage: 'Warning: Potential broken links',
13
+ },
14
+ loading: {
15
+ id: 'link-integrity: loading references',
16
+ defaultMessage: 'Checking references...',
17
+ },
18
+ confirmAction: {
19
+ id: 'link-integrity: Change state anyway',
20
+ defaultMessage: 'Change state anyway',
21
+ },
22
+ cancel: {
23
+ id: 'Cancel',
24
+ defaultMessage: 'Cancel',
25
+ },
26
+ navigate_to_this_item: {
27
+ id: 'Navigate to this item',
28
+ defaultMessage: 'Navigate to this item',
29
+ },
30
+ });
31
+
32
+ const WorkflowLinkIntegrityModal = (props) => {
33
+ const { open, onCancel, onOk } = props;
34
+ const intl = useIntl();
35
+ const linkintegrityInfo = useSelector((state) => state.linkIntegrity?.result);
36
+ const loading = useSelector((state) => state.linkIntegrity?.loading);
37
+
38
+ const [brokenReferences, setBrokenReferences] = useState(0);
39
+ const [breaches, setBreaches] = useState([]);
40
+
41
+ useEffect(() => {
42
+ if (linkintegrityInfo) {
43
+ const breaches = linkintegrityInfo.flatMap((result) =>
44
+ result.breaches.map((source) => ({
45
+ source: source,
46
+ target: result,
47
+ })),
48
+ );
49
+ const source_by_uid = breaches.reduce(
50
+ (acc, value) => acc.set(value.source.uid, value.source),
51
+ new Map(),
52
+ );
53
+ const by_source = breaches.reduce((acc, value) => {
54
+ if (acc.get(value.source.uid) === undefined) {
55
+ acc.set(value.source.uid, new Set());
56
+ }
57
+ acc.get(value.source.uid).add(value.target);
58
+ return acc;
59
+ }, new Map());
60
+
61
+ setBrokenReferences(by_source.size);
62
+ setBreaches(
63
+ Array.from(by_source, (entry) => ({
64
+ source: source_by_uid.get(entry[0]),
65
+ targets: Array.from(entry[1]),
66
+ })),
67
+ );
68
+ } else {
69
+ setBrokenReferences(0);
70
+ setBreaches([]);
71
+ }
72
+ }, [linkintegrityInfo]);
73
+
74
+ // If we are still loading, show the dimmer
75
+ if (loading && open) {
76
+ return (
77
+ <Confirm
78
+ open={open}
79
+ header={intl.formatMessage(messages.confirmHeader)}
80
+ content={
81
+ <div className="content">
82
+ <Dimmer active inverted>
83
+ <Loader indeterminate size="massive">
84
+ {intl.formatMessage(messages.loading)}
85
+ </Loader>
86
+ </Dimmer>
87
+ <div style={{ minHeight: '100px' }} />
88
+ </div>
89
+ }
90
+ onCancel={onCancel}
91
+ onConfirm={() => {}}
92
+ />
93
+ );
94
+ }
95
+
96
+ return (
97
+ open &&
98
+ brokenReferences > 0 && (
99
+ <Confirm
100
+ open={open}
101
+ confirmButton={intl.formatMessage(messages.confirmAction)}
102
+ cancelButton={intl.formatMessage(messages.cancel)}
103
+ header={intl.formatMessage(messages.confirmHeader)}
104
+ content={
105
+ <div className="content">
106
+ <FormattedMessage
107
+ id="Changing the state of this item will break {brokenReferences} {variation} to it."
108
+ defaultMessage="Changing the state of this item will break {brokenReferences} {variation} to it."
109
+ values={{
110
+ brokenReferences: <span>{brokenReferences}</span>,
111
+ variation: (
112
+ <span>
113
+ {brokenReferences === 1 ? (
114
+ <FormattedMessage
115
+ id="reference"
116
+ defaultMessage="reference"
117
+ />
118
+ ) : (
119
+ <FormattedMessage
120
+ id="references"
121
+ defaultMessage="references"
122
+ />
123
+ )}
124
+ </span>
125
+ ),
126
+ }}
127
+ />
128
+ <BrokenLinksList intl={intl} breaches={breaches} />
129
+ </div>
130
+ }
131
+ onCancel={onCancel}
132
+ onConfirm={onOk}
133
+ size="small"
134
+ />
135
+ )
136
+ );
137
+ };
138
+
139
+ const BrokenLinksList = ({ intl, breaches }) => {
140
+ return (
141
+ <div className="broken-links-list" style={{ marginTop: '20px' }}>
142
+ <FormattedMessage
143
+ id="These items will have broken links"
144
+ defaultMessage="These items will have broken links"
145
+ />
146
+ :
147
+ <Table compact>
148
+ <Table.Body>
149
+ {breaches.map((breach) => (
150
+ <Table.Row key={breach.source['@id']} verticalAlign="top">
151
+ <Table.Cell>
152
+ <Link
153
+ to={flattenToAppURL(breach.source['@id'])}
154
+ title={intl.formatMessage(messages.navigate_to_this_item)}
155
+ >
156
+ {breach.source.title}
157
+ </Link>
158
+ </Table.Cell>
159
+ <Table.Cell style={{ minWidth: '140px' }}>
160
+ <FormattedMessage id="refers to" defaultMessage="refers to" />:
161
+ </Table.Cell>
162
+ <Table.Cell>
163
+ <ul style={{ margin: 0 }}>
164
+ {breach.targets.map((target) => (
165
+ <li key={target['@id']}>
166
+ <Link
167
+ to={flattenToAppURL(target['@id'])}
168
+ title={intl.formatMessage(
169
+ messages.navigate_to_this_item,
170
+ )}
171
+ >
172
+ {target.title}
173
+ </Link>
174
+ </li>
175
+ ))}
176
+ </ul>
177
+ </Table.Cell>
178
+ </Table.Row>
179
+ ))}
180
+ </Table.Body>
181
+ </Table>
182
+ </div>
183
+ );
184
+ };
185
+
186
+ WorkflowLinkIntegrityModal.propTypes = {
187
+ open: PropTypes.bool.isRequired,
188
+ onOk: PropTypes.func.isRequired,
189
+ onCancel: PropTypes.func.isRequired,
190
+ };
191
+
192
+ export default WorkflowLinkIntegrityModal;