@eeacms/volto-marine-policy 2.0.17 → 2.0.19

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.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [2.0.19](https://github.com/eea/volto-marine-policy/compare/2.0.18...2.0.19) - 25 August 2025
8
+
9
+ #### :house: Internal changes
10
+
11
+ - style: Automated code fix [eea-jenkins - [`de9990e`](https://github.com/eea/volto-marine-policy/commit/de9990e8922e4f56f97cc7a84c081a4597ab8ee7)]
12
+
13
+ #### :hammer_and_wrench: Others
14
+
15
+ - fix NIS [laszlocseh - [`cf15edd`](https://github.com/eea/volto-marine-policy/commit/cf15eddc8efe127bf29c6a5ed77e86a0a605fa90)]
16
+ ### [2.0.18](https://github.com/eea/volto-marine-policy/compare/2.0.17...2.0.18) - 22 August 2025
17
+
18
+ #### :house: Internal changes
19
+
20
+ - style: Automated code fix [eea-jenkins - [`58ad0aa`](https://github.com/eea/volto-marine-policy/commit/58ad0aa67a4d13e03c1bf338e904eb07cef47dba)]
21
+ - style: Automated code fix [eea-jenkins - [`25f767c`](https://github.com/eea/volto-marine-policy/commit/25f767ce4e26f68cd3daeebee7e2d0f594e8afd5)]
22
+
23
+ #### :hammer_and_wrench: Others
24
+
25
+ - fix eslint [laszlocseh - [`f494ab2`](https://github.com/eea/volto-marine-policy/commit/f494ab287e04db2c75d413de398e73ae510f6f4c)]
26
+ - fix eslint [laszlocseh - [`0a94318`](https://github.com/eea/volto-marine-policy/commit/0a94318433bc0ae035efa9f3043d63b53645ab67)]
27
+ - WiP NIS listing [laszlocseh - [`eed0dcf`](https://github.com/eea/volto-marine-policy/commit/eed0dcf37c5047db6d85f12e0a14683c8e48884e)]
28
+ - Update Login.jsx [dobri1408 - [`668cbf6`](https://github.com/eea/volto-marine-policy/commit/668cbf619d63d51062f869c3e3b3d180e64f223e)]
29
+ - entraid [Dobricean Ioan Dorian - [`c573e9b`](https://github.com/eea/volto-marine-policy/commit/c573e9bcddba9dab7ddea558d935365ea905b282)]
7
30
  ### [2.0.17](https://github.com/eea/volto-marine-policy/compare/2.0.16...2.0.17) - 8 August 2025
8
31
 
9
32
  #### :rocket: Dependency updates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-marine-policy",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
4
4
  "description": "@eeacms/volto-marine-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -24,7 +24,8 @@
24
24
  "@eeacms/volto-eea-website-theme",
25
25
  "@eeacms/volto-globalsearch",
26
26
  "@eeacms/volto-metadata-block",
27
- "@eeacms/volto-workflow-progress"
27
+ "@eeacms/volto-workflow-progress",
28
+ "@plone-collective/volto-authomatic"
28
29
  ],
29
30
  "resolutions": {
30
31
  "react-countup/countup.js": "2.5.0",
@@ -37,11 +38,12 @@
37
38
  "@eeacms/volto-eea-website-theme": "*",
38
39
  "@eeacms/volto-embed": "*",
39
40
  "@eeacms/volto-globalsearch": "2.1.2",
41
+ "@eeacms/volto-metadata-block": "*",
40
42
  "@eeacms/volto-openlayers-map": "1.0.1",
41
43
  "@eeacms/volto-searchlib": "2.1.8",
42
44
  "@eeacms/volto-tabs-block": "*",
43
- "@eeacms/volto-metadata-block": "*",
44
45
  "@eeacms/volto-workflow-progress": "*",
46
+ "@plone-collective/volto-authomatic": "2.0.1",
45
47
  "axios": "0.30.0",
46
48
  "d3-array": "^2.12.1",
47
49
  "jquery": "3.6.0",
@@ -4,99 +4,193 @@ import ProgressWorkflow from '@eeacms/volto-marine-policy/components/theme/Progr
4
4
  // import { Link } from 'react-router-dom';
5
5
  import PropTypes from 'prop-types';
6
6
  import './style.less';
7
-
8
- // const NISListingView = ({ items, isEditMode }) => {
9
- // console.log(items);
10
- // return (
11
- // <div className="items">
12
- // {items.map((item, index) => (
13
- // <div className="listing-item" key={item['@id']}>
14
- // <div className="listing-body">
15
- // <h3>
16
- // <Link to={item['@id']}>{item.title}</Link>
17
- // </h3>
18
- // <div className="listing-metadata">
19
- // {item.nis_species_name_accepted && (
20
- // <div className="metadata-item">
21
- // <b>Species name accepted:</b> {item.nis_species_name_accepted}
22
- // </div>
23
- // )}
24
- // {item.nis_region && (
25
- // <div className="metadata-item">
26
- // <b>Region:</b> {item.nis_region}
27
- // </div>
28
- // )}
29
- // {item.nis_subregion && (
30
- // <div className="metadata-item">
31
- // <b>Subregion:</b> {item.nis_subregion}
32
- // </div>
33
- // )}
34
- // </div>
35
- // </div>
36
- // </div>
37
- // ))}
38
- // </div>
39
- // );
40
- // };
7
+ import { useState, useEffect } from 'react';
8
+ import { useSelector } from 'react-redux';
9
+ import { Checkbox } from 'semantic-ui-react';
10
+ import { Button, Select } from 'semantic-ui-react';
41
11
 
42
12
  const NISListingView = ({ items, isEditMode }) => {
43
- // console.log(items);
13
+ const [selectedItems, setSelectedItems] = useState([]);
14
+ const [users, setUsers] = useState([]);
15
+ const [assignee, setAssignee] = useState(null);
16
+ const actions = useSelector((state) => state.actions.actions);
17
+ const canEditPage = actions?.object?.some((action) => action.id === 'edit');
18
+ // console.log('actions', actions);
19
+
20
+ const toggleSelection = (id) => {
21
+ setSelectedItems((prev) =>
22
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
23
+ );
24
+ };
25
+
26
+ const handleBulkAssign = () => {
27
+ onBulkAssign(selectedItems, assignee);
28
+ setSelectedItems([]);
29
+ setAssignee(null);
30
+ };
31
+
32
+ const onBulkAssign = async (ids, assignee) => {
33
+ await fetch(`${window.location.origin}/++api++/@bulk-assign`, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ Accept: 'application/json',
38
+ },
39
+ credentials: 'include',
40
+ body: JSON.stringify({
41
+ items: ids,
42
+ assigned_to: assignee,
43
+ }),
44
+ });
45
+
46
+ // const result = await res.json();
47
+ window.location.reload();
48
+ };
49
+
50
+ useEffect(() => {
51
+ const fetchUsers = async () => {
52
+ const res = await fetch(
53
+ `${window.location.origin}/++api++/@vocabularies/nis_experts_vocabulary`,
54
+ {
55
+ headers: {
56
+ Accept: 'application/json',
57
+ },
58
+ credentials: 'include',
59
+ },
60
+ );
61
+ const data = await res.json();
62
+ if (data?.items) {
63
+ setUsers(
64
+ data.items.map((u) => ({
65
+ key: u.token,
66
+ text: u.title,
67
+ value: u.token,
68
+ })),
69
+ );
70
+ }
71
+ };
72
+ fetchUsers();
73
+ }, []);
44
74
 
45
75
  return (
46
- <table className="ui table">
47
- <thead>
48
- <tr>
49
- <th>Species name original</th>
50
- <th>Species name accepted</th>
51
- <th>Scientific name accepted</th>
52
- <th>Region</th>
53
- <th>Subregion</th>
54
- <th>Status</th>
55
- <th>Group</th>
56
- <th></th>
57
- </tr>
58
- </thead>
59
- <tbody>
60
- {items.map((item, index) => (
61
- <tr key={item['@id']}>
62
- <td>{item.nis_species_name_original}</td>
63
- <td>{item.nis_species_name_accepted}</td>
64
- <td>{item.nis_scientificname_accepted}</td>
65
- <td>{item.nis_region}</td>
66
- <td>{item.nis_subregion}</td>
67
- <td>{item.nis_status}</td>
68
- <td>{item.nis_group}</td>
69
- <td className="workflow-actions">
70
- <div className="action-buttons">
71
- <a
72
- className="ui button secondary mini"
73
- href={`${item['@id']}`}
74
- target="_blank"
75
- rel="noopener"
76
- >
77
- View
78
- </a>
79
- <a
80
- className="ui button primary mini"
81
- href={`${item['@id']}/edit`}
82
- target="_blank"
83
- rel="noopener"
84
- >
85
- Edit
86
- </a>
87
- </div>
88
- <div className="workflow-progress">
89
- <ProgressWorkflow
90
- content={item}
91
- pathname={item['@id']}
92
- token={123}
93
- />
94
- </div>
95
- </td>
76
+ <>
77
+ <table className="ui table">
78
+ <thead>
79
+ <tr>
80
+ <th>Species name original</th>
81
+ <th>Species name accepted</th>
82
+ <th>Scientific name accepted</th>
83
+ <th>Region</th>
84
+ <th>Subregion</th>
85
+ <th>Status</th>
86
+ <th>Group</th>
87
+ <th>Assigned to</th>
88
+ <th>
89
+ {canEditPage && (
90
+ <div>
91
+ <a
92
+ href={`/++api++${window.location.pathname}/nis-export${window.location.search}`}
93
+ title="Download"
94
+ target="_blank"
95
+ rel="noopener"
96
+ className="ui button primary download-as-xls"
97
+ >
98
+ <i className="ri-file-download-line"></i>
99
+ Download
100
+ </a>
101
+ </div>
102
+ )}
103
+ </th>
96
104
  </tr>
97
- ))}
98
- </tbody>
99
- </table>
105
+ </thead>
106
+ <tbody>
107
+ {items.map((item, index) => (
108
+ <tr key={item['@id']}>
109
+ <td>{item.nis_species_name_original}</td>
110
+ <td>{item.nis_species_name_accepted}</td>
111
+ <td>{item.nis_scientificname_accepted}</td>
112
+ <td>{item.nis_region}</td>
113
+ <td>{item.nis_subregion}</td>
114
+ <td>{item.nis_status}</td>
115
+ <td>{item.nis_group}</td>
116
+ <td>
117
+ <div className="assigned-to-container">
118
+ <div>{item.nis_assigned_to}</div>
119
+ {canEditPage && (
120
+ <Checkbox
121
+ checked={selectedItems.includes(item['@id'])}
122
+ onChange={() => toggleSelection(item['@id'])}
123
+ />
124
+ )}
125
+ </div>
126
+ </td>
127
+ <td>
128
+ <div className="workflow-actions">
129
+ <div className="action-buttons">
130
+ <a
131
+ className="ui button secondary mini"
132
+ href={`${item['@id']}`}
133
+ target="_blank"
134
+ rel="noopener"
135
+ >
136
+ View
137
+ </a>
138
+ <a
139
+ className="ui button primary mini"
140
+ href={`${item['@id']}/edit`}
141
+ target="_blank"
142
+ rel="noopener"
143
+ >
144
+ Edit
145
+ </a>
146
+ </div>
147
+ <div className="workflow-progress">
148
+ <ProgressWorkflow
149
+ content={item}
150
+ pathname={item['@id']}
151
+ token={123}
152
+ />
153
+ </div>
154
+ </div>
155
+ </td>
156
+ </tr>
157
+ ))}
158
+ </tbody>
159
+ </table>
160
+ {selectedItems.length > 0 && (
161
+ <div className="users-assign-container">
162
+ <h4>
163
+ Assign {selectedItems.length} item
164
+ {selectedItems.length > 1 ? 's' : ''}
165
+ </h4>
166
+ <Select
167
+ placeholder="Select expert"
168
+ options={users}
169
+ value={assignee}
170
+ onChange={(e, { value }) => setAssignee(value)}
171
+ />
172
+ <div style={{ marginTop: '10px', textAlign: 'right' }}>
173
+ <Button
174
+ className="tertiary"
175
+ size="small"
176
+ onClick={() => setSelectedItems([])}
177
+ style={{ marginRight: '5px' }}
178
+ >
179
+ Cancel
180
+ </Button>
181
+ <Button
182
+ className="primary"
183
+ size="small"
184
+ // color="green"
185
+ disabled={!assignee}
186
+ onClick={handleBulkAssign}
187
+ >
188
+ Assign
189
+ </Button>
190
+ </div>
191
+ </div>
192
+ )}{' '}
193
+ </>
100
194
  );
101
195
  };
102
196
 
@@ -14,10 +14,10 @@
14
14
  .ui.table tr {
15
15
  td,
16
16
  th {
17
- border-left: 1px solid #bcbec0 !important;
17
+ border-right: 1px solid #bcbec0 !important;
18
18
 
19
- &:first-of-type {
20
- border-left: none !important;
19
+ &:last-of-type {
20
+ border-right: none !important;
21
21
  }
22
22
  }
23
23
  }
@@ -119,3 +119,29 @@
119
119
  border-color: #3ca34a !important;
120
120
  color: #3ca34a !important;
121
121
  }
122
+
123
+ .users-assign-container {
124
+ position: fixed;
125
+ z-index: 1000;
126
+ top: 20px;
127
+ right: 20px;
128
+ padding: 16px;
129
+ border: 1px solid #ccc;
130
+ border-radius: 8px;
131
+ background: white;
132
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
133
+ }
134
+
135
+ .assigned-to-container {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ }
140
+
141
+ .ri-file-download-line {
142
+ margin-right: 5px;
143
+ }
144
+
145
+ .download-as-xls {
146
+ padding: 0.6em 0.5em !important;
147
+ }
@@ -1,113 +1,3 @@
1
- // import config from '@plone/volto/registry';
2
- // import { useSelector } from 'react-redux';
3
- // import { Table } from 'semantic-ui-react';
4
- // import { ErrorBoundary } from '@eeacms/volto-metadata-block/widgets';
5
- // import { isEmpty } from 'lodash';
6
- // import { useIntl } from 'react-intl';
7
- // import './style.less';
8
-
9
- // function isEmptyWithNumberCheck(value) {
10
- // // Check if the value is a number and is not NaN
11
- // if (typeof value === 'number' && !isNaN(value)) {
12
- // return false; // Numbers, including 0, are considered non-empty
13
- // }
14
-
15
- // // Fallback to Lodash's isEmpty for other types
16
- // return isEmpty(value);
17
- // }
18
-
19
- // const Field = (props) => {
20
- // const { data, properties = {}, metadata = {}, showLabel } = props;
21
- // const { views } = config.widgets;
22
- // const intl = useIntl();
23
-
24
- // const initialFormData = useSelector((state) => state?.content?.data || {});
25
- // const dataTitle = data?.title;
26
- // const label = intl.formatMessage({ id: dataTitle, message: dataTitle });
27
-
28
- // let metadata_element = {
29
- // ...initialFormData,
30
- // ...(properties || {}),
31
- // ...(metadata || {}),
32
- // };
33
-
34
- // if (!data?.id) {
35
- // return '';
36
- // }
37
-
38
- // let output = metadata_element[data.id];
39
-
40
- // let Widget = views?.getWidget(data);
41
- // if (!output && props.data.placeholder) {
42
- // Widget = views?.default;
43
- // output = props.data.placeholder;
44
- // }
45
-
46
- // const hasValue = !isEmptyWithNumberCheck(output);
47
-
48
- // if (!Widget || !hasValue) {
49
- // return '';
50
- // }
51
-
52
- // let className = 'block metadata ' + data.id;
53
- // return (
54
- // <ErrorBoundary name={data.id}>
55
- // {showLabel ? (
56
- // <label htmlFor={`metadata-${data.id}`} className={className}>
57
- // {label}
58
- // </label>
59
- // ) : (
60
- // ''
61
- // )}
62
- // <Widget
63
- // value={output}
64
- // content={metadata_element}
65
- // className={className}
66
- // id={`metadata-${data.id}`}
67
- // />
68
- // </ErrorBoundary>
69
- // );
70
- // };
71
-
72
- // const NISMetadataSectionTableView = (props) => {
73
- // const { data = {}, properties = {}, metadata = {} } = props;
74
- // const { table = {}, fields = [] } = data;
75
-
76
- // const initialFormData = useSelector((state) => state?.content?.data || {});
77
- // let metadata_element = { ...initialFormData, ...properties, ...metadata };
78
- // const showFields = fields.filter(({ hideInView }) => !hideInView);
79
-
80
- // return showFields.length ? (
81
- // <Table
82
- // fixed={table.fixed}
83
- // compact={table.compact}
84
- // basic={table.basic ? 'very' : false}
85
- // celled={table.celled}
86
- // inverted={table.inverted}
87
- // striped={table.striped}
88
- // >
89
- // <Table.Body>
90
- // {showFields.map(({ field }, i) => {
91
- // const hasValue = !isEmptyWithNumberCheck(metadata_element[field?.id]);
92
-
93
- // return hasValue ? (
94
- // <Table.Row key={i}>
95
- // <Table.HeaderCell width={1}>{field.title}</Table.HeaderCell>
96
- // <Table.Cell>
97
- // <Field key={i} {...props} showLabel={false} data={field} />
98
- // </Table.Cell>
99
- // </Table.Row>
100
- // ) : (
101
- // ''
102
- // );
103
- // })}
104
- // </Table.Body>
105
- // </Table>
106
- // ) : (
107
- // ''
108
- // );
109
- // };
110
-
111
1
  import { MetadataSectionTableView } from '@eeacms/volto-metadata-block/components';
112
2
  import './style.less';
113
3
 
@@ -249,8 +249,8 @@ const ProgressWorkflow = (props) => {
249
249
  const rect = button.getBoundingClientRect();
250
250
 
251
251
  dropdown.style.position = 'fixed';
252
- dropdown.style.top = `${rect.bottom}px`; // or rect.top
253
- dropdown.style.left = `${rect.left}px`;
252
+ dropdown.style.top = `${rect.bottom - 35}px`; // or rect.top
253
+ dropdown.style.left = `${rect.left - 200}px`;
254
254
  dropdown.classList.toggle('is-hidden');
255
255
  // console.log(rect.bottom, rect.left);
256
256
  };
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Combined Login container - supports both external providers and Plone login.
3
+ * @module components/Login/Login
4
+ */
5
+ import React, { useEffect, useState } from 'react';
6
+ import { useDispatch, useSelector, shallowEqual } from 'react-redux';
7
+ import { Link, useHistory, useLocation } from 'react-router-dom';
8
+ import {
9
+ Container,
10
+ Button,
11
+ Form,
12
+ Input,
13
+ Segment,
14
+ Grid,
15
+ } from 'semantic-ui-react';
16
+ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
17
+ import qs from 'query-string';
18
+ import { useCookies } from 'react-cookie';
19
+
20
+ import { Helmet } from '@plone/volto/helpers';
21
+ import config from '@plone/volto/registry';
22
+ import { Icon } from '@plone/volto/components';
23
+ import { login, resetLoginRequest } from '@plone/volto/actions';
24
+ import { toast } from 'react-toastify';
25
+ import { Toast } from '@plone/volto/components';
26
+ import aheadSVG from '@plone/volto/icons/ahead.svg';
27
+ import clearSVG from '@plone/volto/icons/clear.svg';
28
+ import './Login.less';
29
+
30
+ // Import authomatic components and actions
31
+ import {
32
+ authomaticRedirect,
33
+ listAuthOptions,
34
+ oidcRedirect,
35
+ } from '@plone-collective/volto-authomatic/actions';
36
+ import AuthProviders from '@plone-collective/volto-authomatic/components/AuthProviders/AuthProviders';
37
+
38
+ const messages = defineMessages({
39
+ login: {
40
+ id: 'Log in',
41
+ defaultMessage: 'Log in',
42
+ },
43
+ loginName: {
44
+ id: 'Login Name',
45
+ defaultMessage: 'Login Name',
46
+ },
47
+ Login: {
48
+ id: 'Login',
49
+ defaultMessage: 'Login',
50
+ },
51
+ password: {
52
+ id: 'Password',
53
+ defaultMessage: 'Password',
54
+ },
55
+ cancel: {
56
+ id: 'Cancel',
57
+ defaultMessage: 'Cancel',
58
+ },
59
+ error: {
60
+ id: 'Error',
61
+ defaultMessage: 'Error',
62
+ },
63
+ loginFailed: {
64
+ id: 'Login Failed',
65
+ defaultMessage: 'Login Failed',
66
+ },
67
+ loginFailedContent: {
68
+ id: 'Both email address and password are case sensitive, check that caps lock is not enabled.',
69
+ defaultMessage:
70
+ 'Both email address and password are case sensitive, check that caps lock is not enabled.',
71
+ },
72
+ register: {
73
+ id: 'Register',
74
+ defaultMessage: 'Register',
75
+ },
76
+ forgotPassword: {
77
+ id: 'box_forgot_password_option',
78
+ defaultMessage: 'Forgot your password?',
79
+ },
80
+ signInWith: {
81
+ id: 'Sign in with EEA Microsoft Entra ID',
82
+ defaultMessage: 'Sign in with EEA Microsoft Entra ID',
83
+ },
84
+ orSignIn: {
85
+ id: 'Or sign in with EEA Entra ID:',
86
+ defaultMessage: 'Or sign in with EEA Entra ID:',
87
+ },
88
+ loading: {
89
+ id: 'Loading',
90
+ defaultMessage: 'Loading',
91
+ },
92
+ });
93
+
94
+ /**
95
+ * Get return url function.
96
+ * @function getReturnUrl
97
+ * @param {Object} location Location object.
98
+ * @returns {string} Return url.
99
+ */
100
+ function getReturnUrl(location) {
101
+ return `${
102
+ qs.parse(location.search).return_url ||
103
+ (location.pathname === '/login'
104
+ ? '/'
105
+ : location.pathname.replace('/login', ''))
106
+ }`;
107
+ }
108
+
109
+ /**
110
+ * Combined Login function.
111
+ * @function Login
112
+ * @returns {JSX.Element} Markup of the Login page.
113
+ */
114
+ function Login({ intl }) {
115
+ const dispatch = useDispatch();
116
+ const history = useHistory();
117
+ const location = useLocation();
118
+
119
+ // Authomatic state
120
+ const [startedOAuth, setStartedOAuth] = useState(false);
121
+ const [startedOIDC, setStartedOIDC] = useState(false);
122
+ const loading = useSelector((state) => state.authOptions.loading);
123
+ const options = useSelector((state) => state.authOptions.options);
124
+ const loginOAuthValues = useSelector((state) => state.authomaticRedirect);
125
+ const loginOIDCValues = useSelector((state) => state.oidcRedirect);
126
+ const [, setCookie] = useCookies();
127
+
128
+ // Plone login state
129
+ const token = useSelector((state) => state.userSession.token, shallowEqual);
130
+ const error = useSelector((state) => state.userSession.login.error);
131
+ const ploneLoading = useSelector((state) => state.userSession.login.loading);
132
+
133
+ const returnUrl =
134
+ qs.parse(location.search).return_url ||
135
+ location.pathname.replace(/\/login\/?$/, '').replace(/\/logout\/?$/, '') ||
136
+ '/';
137
+
138
+ useEffect(() => {
139
+ dispatch(listAuthOptions());
140
+ }, [dispatch]);
141
+
142
+ // Handle successful Plone login
143
+ useEffect(() => {
144
+ if (token) {
145
+ history.push(returnUrl || '/');
146
+ if (toast.isActive('loggedOut')) {
147
+ toast.dismiss('loggedOut');
148
+ }
149
+ if (toast.isActive('loginFailed')) {
150
+ toast.dismiss('loginFailed');
151
+ }
152
+ }
153
+ if (error) {
154
+ if (toast.isActive('loggedOut')) {
155
+ toast.dismiss('loggedOut');
156
+ }
157
+ if (!toast.isActive('loginFailed')) {
158
+ toast.error(
159
+ <Toast
160
+ error
161
+ title={intl.formatMessage(messages.loginFailed)}
162
+ content={intl.formatMessage(messages.loginFailedContent)}
163
+ />,
164
+ { autoClose: false, toastId: 'loginFailed' },
165
+ );
166
+ }
167
+ }
168
+ return () => {
169
+ if (toast.isActive('loginFailed')) {
170
+ toast.dismiss('loginFailed');
171
+ dispatch(resetLoginRequest());
172
+ }
173
+ };
174
+ }, [dispatch, token, error, intl, history, returnUrl]);
175
+
176
+ // Handle OAuth redirects
177
+ useEffect(() => {
178
+ const next_url = loginOAuthValues.next_url;
179
+ const session = loginOAuthValues.session;
180
+ if (next_url && session && startedOAuth) {
181
+ setStartedOAuth(false);
182
+ // Give time to save state to localstorage
183
+ setTimeout(function () {
184
+ window.location.href = next_url;
185
+ }, 500);
186
+ }
187
+ }, [startedOAuth, loginOAuthValues]);
188
+
189
+ useEffect(() => {
190
+ const next_url = loginOIDCValues.next_url;
191
+ if (next_url && startedOIDC) {
192
+ setStartedOIDC(false);
193
+ // Give time to save state to localstorage
194
+ setTimeout(function () {
195
+ window.location.href = next_url;
196
+ }, 500);
197
+ }
198
+ }, [startedOIDC, loginOIDCValues]);
199
+
200
+ useEffect(() => {
201
+ if (
202
+ options !== undefined &&
203
+ options.length === 1 &&
204
+ options[0].id === 'oidc'
205
+ ) {
206
+ setStartedOIDC(true);
207
+ dispatch(oidcRedirect('oidc'));
208
+ }
209
+ }, [options, dispatch]);
210
+
211
+ // Handle provider selection
212
+ const onSelectProvider = (provider) => {
213
+ setStartedOAuth(true);
214
+ setCookie('return_url', getReturnUrl(location), { path: '/' });
215
+ dispatch(authomaticRedirect(provider.id));
216
+ };
217
+
218
+ // Handle Plone login form submission
219
+ const onLogin = (event) => {
220
+ dispatch(
221
+ login(
222
+ document.getElementsByName('login')[0].value,
223
+ document.getElementsByName('password')[0].value,
224
+ ),
225
+ );
226
+ event.preventDefault();
227
+ };
228
+
229
+ // Prepare providers for external login
230
+ const validProviders = options
231
+ ? options.filter((provider) => provider.id !== 'oidc')
232
+ : [];
233
+
234
+ return (
235
+ <div id="page-login">
236
+ <Helmet title={intl.formatMessage(messages.Login)} />
237
+ <Container text>
238
+ <Segment.Group raised>
239
+ <Segment className="primary">
240
+ <FormattedMessage id="Log In" defaultMessage="Login" />
241
+ </Segment>
242
+ <Segment secondary>
243
+ <FormattedMessage
244
+ id="Sign in to start session"
245
+ defaultMessage="Sign in to start session"
246
+ />
247
+ </Segment>
248
+
249
+ {/* Plone Login Form */}
250
+ <Segment className="form">
251
+ <Form method="post" onSubmit={onLogin}>
252
+ <Form.Field inline className="help">
253
+ <Grid>
254
+ <Grid.Row stretched>
255
+ <Grid.Column width="4">
256
+ <div className="wrapper">
257
+ <label htmlFor="login">
258
+ <FormattedMessage
259
+ id="Login Name"
260
+ defaultMessage="Login Name"
261
+ />
262
+ </label>
263
+ </div>
264
+ </Grid.Column>
265
+ <Grid.Column width="8">
266
+ <Input
267
+ id="login"
268
+ name="login"
269
+ placeholder={intl.formatMessage(messages.loginName)}
270
+ />
271
+ </Grid.Column>
272
+ </Grid.Row>
273
+ </Grid>
274
+ </Form.Field>
275
+ <Form.Field inline className="help">
276
+ <Grid>
277
+ <Grid.Row stretched>
278
+ <Grid.Column stretched width="4">
279
+ <div className="wrapper">
280
+ <label htmlFor="password">
281
+ <FormattedMessage
282
+ id="Password"
283
+ defaultMessage="Password"
284
+ />
285
+ </label>
286
+ </div>
287
+ </Grid.Column>
288
+ <Grid.Column stretched width="8">
289
+ <Input
290
+ type="password"
291
+ id="password"
292
+ autoComplete="current-password"
293
+ name="password"
294
+ placeholder={intl.formatMessage(messages.password)}
295
+ tabIndex={0}
296
+ />
297
+ </Grid.Column>
298
+ </Grid.Row>
299
+ </Grid>
300
+ </Form.Field>
301
+ <Form.Field inline className="help">
302
+ <Grid>
303
+ <Grid.Row stretched>
304
+ {config.settings.showSelfRegistration && (
305
+ <Grid.Column stretched width="12">
306
+ <p className="help">
307
+ <Link to="/register">
308
+ {intl.formatMessage(messages.register)}
309
+ </Link>
310
+ </p>
311
+ </Grid.Column>
312
+ )}
313
+ <Grid.Column stretched width="12">
314
+ <p className="help">
315
+ <Link to="/passwordreset">
316
+ {intl.formatMessage(messages.forgotPassword)}
317
+ </Link>
318
+ </p>
319
+ </Grid.Column>
320
+ </Grid.Row>
321
+ </Grid>
322
+ </Form.Field>
323
+ </Form>
324
+ </Segment>
325
+
326
+ <Segment className="actions" clearing>
327
+ <Button
328
+ basic
329
+ primary
330
+ icon
331
+ floated="right"
332
+ type="submit"
333
+ form="login-form"
334
+ id="login-form-submit"
335
+ aria-label={intl.formatMessage(messages.login)}
336
+ title={intl.formatMessage(messages.login)}
337
+ loading={ploneLoading}
338
+ onClick={onLogin}
339
+ >
340
+ <Icon className="circled" name={aheadSVG} size="30px" />
341
+ </Button>
342
+
343
+ <Button
344
+ basic
345
+ secondary
346
+ icon
347
+ floated="right"
348
+ id="login-form-cancel"
349
+ as={Link}
350
+ to="/"
351
+ aria-label={intl.formatMessage(messages.cancel)}
352
+ title={intl.formatMessage(messages.cancel)}
353
+ >
354
+ <Icon className="circled" name={clearSVG} size="30px" />
355
+ </Button>
356
+ </Segment>
357
+ </Segment.Group>
358
+
359
+ {/* External Login Providers - Outside the main form */}
360
+ {validProviders && validProviders.length > 0 && (
361
+ <div style={{ marginTop: '2rem', width: '100%' }}>
362
+ <div style={{ textAlign: 'center', marginBottom: '1rem' }}>
363
+ <FormattedMessage
364
+ id="Or sign in with external provider:"
365
+ defaultMessage="Or sign in with external provider:"
366
+ />
367
+ </div>
368
+ <div style={{ width: '100%' }}>
369
+ {!loading && validProviders && (
370
+ <AuthProviders
371
+ providers={validProviders}
372
+ action="login"
373
+ onSelectProvider={onSelectProvider}
374
+ />
375
+ )}
376
+ {(loading || validProviders.length === 0) && (
377
+ <div style={{ textAlign: 'center', padding: '1rem' }}>
378
+ {intl.formatMessage(messages.loading)}
379
+ </div>
380
+ )}
381
+ </div>
382
+ </div>
383
+ )}
384
+ </Container>
385
+ </div>
386
+ );
387
+ }
388
+
389
+ export default injectIntl(Login);
@@ -0,0 +1,8 @@
1
+ .ui.button.authenticationProvider {
2
+ display: inline-flex;
3
+ width: 100% !important;
4
+ align-items: center;
5
+ border-radius: 3px;
6
+ margin: 0.5rem auto;
7
+ box-shadow: rgba(0, 0, 0, 0.5) 0 1px 2px;
8
+ }