Skip to content

Commit a1d9ae8

Browse files
committed
ENH Show collapsed sudo message on GridFields
1 parent 58c0208 commit a1d9ae8

8 files changed

Lines changed: 220 additions & 30 deletions

File tree

client/dist/js/bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/dist/styles/bundle.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/src/components/SudoModePasswordField/SudoModePasswordField.js

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import i18n from 'i18n';
33
import Config from 'lib/Config';
44
import backend from 'lib/Backend';
55
import PropTypes from 'prop-types';
6-
import React, { createRef, useState } from 'react';
6+
import React, { createRef, useState, useEffect } from 'react';
77
import { InputGroup, InputGroupAddon, Input, FormGroup, Label, FormFeedback } from 'reactstrap';
88

99
/**
@@ -15,14 +15,50 @@ function SudoModePasswordField(props) {
1515
const {
1616
onSuccess,
1717
autocomplete,
18+
initiallyCollapsed,
1819
verifyMessage,
20+
sectionTitle,
1921
} = props;
2022
const passwordFieldRef = createRef();
2123
const [responseMessage, setResponseMessage] = useState('');
2224
const [showVerify, setShowVerify] = useState(false);
25+
// setting to null initially to prevent a fout as useEffect()
26+
// below will cause a re-render when initiallyCollapsed
27+
const [expanded, setExpanded] = useState(null);
28+
29+
useEffect(() => {
30+
setExpanded(!initiallyCollapsed);
31+
}, [initiallyCollapsed]);
2332

2433
const clientConfig = Config.getSection('SilverStripe\\Admin\\SudoModeController');
2534

35+
let verifyMessageValue = verifyMessage;
36+
if (!verifyMessageValue) {
37+
if (sectionTitle) {
38+
verifyMessageValue = i18n.inject(
39+
i18n._t(
40+
'Admin.SUDO_MODE_PASSWORD_FIELD_VERIFY_SECTION_TITLE',
41+
'\"{sectionTitle}\" is protected and is in read-only mode. Before editing please verify that it\'s you first.'
42+
),
43+
{ sectionTitle }
44+
);
45+
} else {
46+
verifyMessageValue = i18n._t(
47+
'Admin.SUDO_MODE_PASSWORD_FIELD_VERIFY',
48+
'This section is protected and is in read-only mode. Before editing please verify that it\'s you first.'
49+
);
50+
}
51+
}
52+
const helpLink = clientConfig.helpLink;
53+
if (helpLink) {
54+
verifyMessageValue = <>
55+
{verifyMessageValue}
56+
<a href={helpLink} className="sudo-mode-password-field__notice-help" target="_blank" rel="noopener noreferrer">
57+
{ i18n._t('Admin.WHATS_THIS', 'What is this?') }
58+
</a>
59+
</>;
60+
}
61+
2662
/**
2763
* Handle clicking the button to confirm the sudo mode notice
2864
* and trigger the verify form to be rendered.
@@ -75,27 +111,19 @@ function SudoModePasswordField(props) {
75111
}
76112
}
77113

114+
function handleExpand() {
115+
setExpanded(true);
116+
setShowVerify(true);
117+
}
118+
78119
/**
79120
* Renders a confirmation notice to the user that they will need to verify themselves
80121
* to enter sudo mode.
81122
*/
82123
function renderConfirm() {
83-
const helpLink = clientConfig.helpLink;
84-
let verifyMessageValue = verifyMessage;
85-
if (!verifyMessageValue) {
86-
verifyMessageValue = i18n._t(
87-
'Admin.SUDO_MODE_PASSWORD_FIELD_VERIFY',
88-
'This section is protected and is in read-only mode. Before editing please verify that it\'s you first.'
89-
);
90-
}
91124
return <div className="sudo-mode__notice sudo-mode-password-field__notice--required">
92125
<p className="sudo-mode-password-field__notice-message">
93126
{ verifyMessageValue }
94-
{ helpLink && (
95-
<a href={helpLink} className="sudo-mode-password-field__notice-help" target="_blank" rel="noopener noreferrer">
96-
{ i18n._t('Admin.WHATS_THIS', 'What is this?') }
97-
</a>
98-
) }
99127
</p>
100128
{ !showVerify && (
101129
<Button
@@ -118,7 +146,7 @@ function SudoModePasswordField(props) {
118146
name: 'SudoModePassword',
119147
id: 'SudoModePassword',
120148
className: 'no-change-track',
121-
autocomplete,
149+
autoComplete: autocomplete,
122150
onKeyDown: (evt) => handleVerifyKeyDown(evt),
123151
innerRef: passwordFieldRef,
124152
};
@@ -145,19 +173,38 @@ function SudoModePasswordField(props) {
145173
</div>;
146174
}
147175

176+
// Prevent a fout on initial render when rendering initiallyCollapsed
177+
if (expanded === null) {
178+
return null;
179+
}
180+
148181
// Render the component
149182
return <div className="sudo-mode-password-field">
150-
<div className="sudo-mode-password-field-inner alert alert-info panel panel--padded">
183+
{ !expanded && <div className="sudo-mode-password-field__expander alert alert-info panel">
184+
<div className="sudo-mode-password-field__expander-text-container">{ verifyMessageValue }</div>
185+
<div className="sudo-mode-password-field__expander-button-container">
186+
<Button
187+
className="sudo-mode-password-field__expander-button font-icon-lock"
188+
color="info"
189+
onClick={() => handleExpand()}
190+
>
191+
{ i18n._t('Admin.VERIFY', 'Verify') }
192+
</Button>
193+
</div>
194+
</div>}
195+
{ expanded && <div className="sudo-mode-password-field__inner alert alert-info panel panel--padded">
151196
{ renderConfirm() }
152197
{ showVerify && renderVerify() }
153-
</div>
198+
</div> }
154199
</div>;
155200
}
156201

157202
SudoModePasswordField.propTypes = {
158203
verifyMessage: PropTypes.string,
159204
onSuccess: PropTypes.func.isRequired,
160205
autocomplete: PropTypes.string.isRequired,
206+
initiallyCollapsed: PropTypes.bool.isRequired,
207+
sectionTitle: PropTypes.string.isRequired,
161208
};
162209

163210
export { SudoModePasswordField as Component };

client/src/components/SudoModePasswordField/SudoModePasswordField.scss

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,28 @@
1212
}
1313
}
1414

15+
.SudoModePasswordField--initially-collapsed.form-group {
16+
min-height: 37px;
17+
margin-bottom: 0;
18+
padding-bottom: 0;
19+
}
20+
21+
.SudoModePasswordField--initially-collapsed.form-group .sudo-mode-password-field {
22+
margin-left: 20px;
23+
margin-right: 20px;
24+
}
25+
26+
.SudoModePasswordField--initially-collapsed.form-group:after {
27+
border-bottom: 0;
28+
}
29+
1530
// React component
1631
.sudo-mode-password-field {
32+
font-size: $font-size-base;
33+
1734
@include media-breakpoint-up(lg) {
1835
width: 100%;
19-
max-width: 700px;
36+
max-width: 800px;
2037
margin-left: $form-check-input-gutter;
2138
}
2239

@@ -29,6 +46,21 @@
2946
}
3047
}
3148

49+
.sudo-mode-password-field__expander {
50+
display: flex;
51+
align-items: center;
52+
margin-bottom: 0;
53+
}
54+
55+
.sudo-mode-password-field__expander-text-container {
56+
margin-left: 4px;
57+
margin-right: 10px;
58+
}
59+
60+
.sudo-mode-password-field__expander-button-container {
61+
margin-left: auto;
62+
}
63+
3264
.sudo-mode-password-field__inner {
3365
margin-bottom: 0;
3466
padding-bottom: 1rem;

client/src/components/SudoModePasswordField/tests/SudoModePasswordField-story.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,28 @@ export default {
3030
}
3131
},
3232
argTypes: {
33+
sectionTitle: {
34+
description: 'The title of the section that is being protected.',
35+
control: 'text',
36+
type: {
37+
required: false
38+
},
39+
table: {
40+
type: { summary: 'string' },
41+
defaultValue: { summary: 'My special data' },
42+
},
43+
},
44+
collapsed: {
45+
description: 'Whether the password field should be collapsed by default.',
46+
control: 'boolean',
47+
type: {
48+
required: true
49+
},
50+
table: {
51+
type: { summary: 'boolean' },
52+
defaultValue: { summary: 'false' },
53+
},
54+
},
3355
autocomplete: {
3456
description: 'The autocomplete attribute for the password field.',
3557
control: 'text',
@@ -44,8 +66,13 @@ export default {
4466
}
4567
};
4668

47-
export const _SudoModePasswordField = (props) => <SudoModePasswordField
48-
{...props}
49-
onSuccess={() => {}}
50-
autocomplete={props.autocomplete || 'off'}
51-
/>;
69+
export const _SudoModePasswordField = (props) => {
70+
const newProps = {
71+
...props,
72+
onSuccess: () => {},
73+
autocomplete: props.autocomplete || 'off',
74+
collapsed: props.hasOwnProperty('collapsed') ? props.collapsed : false,
75+
sectionTitle: props.sectionTitle || 'My special data',
76+
};
77+
return <SudoModePasswordField {...newProps}/>;
78+
};

client/src/components/SudoModePasswordField/tests/SudoModePasswordField-test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ function makeProps(obj = {}) {
2929
return {
3030
onSuccess: () => {},
3131
autocomplete: 'off',
32+
initiallyCollapsed: false,
33+
sectionTitle: '',
3234
...obj,
3335
};
3436
}
@@ -76,3 +78,50 @@ test('SudoModePasswordField should show a message on failure', async () => {
7678
expect(message).not.toBeNull();
7779
expect(onSuccess).not.toBeCalled();
7880
});
81+
82+
test('SudoModePasswordField when initiallyCollapsed is false', async () => {
83+
const { container } = render(
84+
<SudoModePasswordField {...makeProps({
85+
initiallyCollapsed: false,
86+
})}
87+
/>
88+
);
89+
const action = await screen.findByText('Verify to continue');
90+
expect(container.querySelector('.sudo-mode-password-field__notice-button')).not.toBeNull();
91+
expect(container.querySelector('.sudo-mode-password-field__expander')).toBeNull();
92+
fireEvent.click(action);
93+
await screen.findByText('Enter your password');
94+
expect(container.querySelector('.sudo-mode-password-field__verify-button')).not.toBeNull();
95+
});
96+
97+
test('SudoModePasswordField when initiallyCollapsed is true', async () => {
98+
const { container } = render(
99+
<SudoModePasswordField {...makeProps({
100+
initiallyCollapsed: true,
101+
})}
102+
/>
103+
);
104+
const action = await screen.findByText('Verify');
105+
expect(container.querySelector('.sudo-mode-password-field__notice-button')).toBeNull();
106+
expect(container.querySelector('.sudo-mode-password-field__expander')).not.toBeNull();
107+
fireEvent.click(action);
108+
await screen.findByText('Enter your password');
109+
expect(container.querySelector('.sudo-mode-password-field__verify-button')).not.toBeNull();
110+
});
111+
112+
test('SudoModePasswordField when sectionTitle is empty', async () => {
113+
const { container } = render(<SudoModePasswordField {...makeProps()}/>);
114+
await screen.findByText('Verify to continue');
115+
const expected = "This section is protected and is in read-only mode. Before editing please verify that it's you first.";
116+
expect(container.querySelector('.sudo-mode-password-field__notice-message').innerHTML).toBe(expected);
117+
});
118+
119+
test('SudoModePasswordField when sectionTitle is not empty', async () => {
120+
const { container } = render(<SudoModePasswordField {...makeProps({
121+
sectionTitle: 'Hello world',
122+
})}
123+
/>);
124+
await screen.findByText('Verify to continue');
125+
const expected = "\"Hello world\" is protected and is in read-only mode. Before editing please verify that it's you first.";
126+
expect(container.querySelector('.sudo-mode-password-field__notice-message').innerHTML).toBe(expected);
127+
});

client/src/legacy/SudoModePasswordField/SudoModePasswordFieldEntwine.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jQuery.entwine('ss', ($) => {
99
// we only want to run this on the field holder, hence the `:not(:input)`
1010
$('.js-injector-boot .SudoModePasswordField:not(:input)').entwine({
1111
ReactRoot: null,
12+
ForGridField: null,
1213

1314
onmatch() {
1415
this._super();
@@ -19,9 +20,12 @@ jQuery.entwine('ss', ($) => {
1920

2021
const SudoModePasswordField = loadComponent('SudoModePasswordField', context);
2122
const input = this.find('input.SudoModePasswordField')[0];
23+
console.log(input);
2224
const props = {
2325
autocomplete: input.getAttribute('autocomplete'),
24-
onSuccess: () => this.reloadPage(),
26+
initiallyCollapsed: input.getAttribute('data-initially-collapsed'),
27+
sectionTitle: input.getAttribute('data-section-title') || '',
28+
onSuccess: () => this.reloadSection(),
2529
};
2630

2731
let root = this.getReactRoot();
@@ -31,6 +35,7 @@ jQuery.entwine('ss', ($) => {
3135

3236
root.render(<SudoModePasswordField {...props}/>);
3337
this.setReactRoot(root);
38+
this.setForGridField(input.hasAttribute('data-for-gridfield'));
3439
},
3540

3641
onunmatch() {
@@ -47,8 +52,17 @@ jQuery.entwine('ss', ($) => {
4752
* This is here instead of inside the react component because in a pure react form the success handler
4853
* should result in re-rendering the react form with editable fields, instead of reloading the whole page.
4954
*/
50-
reloadPage() {
51-
this.closest('.cms-container').reloadCurrentPanel();
55+
reloadSection() {
56+
if (this.getForGridField()) {
57+
// Reload all sudo protected gridfields (not just this one)
58+
this
59+
.closest('.cms-container')
60+
.find('.ss-gridfield')
61+
.filter((i, el) => jQuery(el).find('.SudoModePasswordField').length)
62+
.reload();
63+
} else {
64+
this.closest('.cms-container').reloadCurrentPanel();
65+
}
5266
},
5367
});
5468
});

tests/behat/features/form-sudo-mode.feature

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ Feature: Form sudo mode
4949
# GridField relation - has_many
5050
When I go to "/admin/pages"
5151
And I click on "My page" in the tree
52-
And I click on the "#Form_EditForm_HasManyMembers .ss-gridfield-item.first" element
52+
Then I should see a "#Form_EditForm_HasManyMembers .sudo-mode-password-field" element
53+
And I should not see a "#Form_EditForm_HasManyMembers .new-link" element
54+
When I click on the "#Form_EditForm_HasManyMembers .ss-gridfield-item.first" element
5355
And I should see a "#Form_ItemEditForm_action_doSave[readonly]" element
5456

5557
# GridField relation - many_many
5658
When I go to "/admin/pages"
5759
And I click on "My page" in the tree
58-
And I click on the "#Form_EditForm_ManyManyMembers .ss-gridfield-item.first" element
60+
Then I should see a "#Form_EditForm_ManyManyMembers .sudo-mode-password-field" element
61+
And I should not see a "#Form_EditForm_ManyManyMembers .new-link" element
62+
When I click on the "#Form_EditForm_ManyManyMembers .ss-gridfield-item.first" element
5963
And I should see a "#Form_ItemEditForm_action_doSave[readonly]" element
6064

6165
Scenario: Putting in a wrong password in to the sudo mode password field shows an error message
@@ -113,3 +117,20 @@ Feature: Form sudo mode
113117
And I click on "My page" in the tree
114118
And I click on the "#Form_EditForm_ManyManyMembers .ss-gridfield-item.first" element
115119
And I should not see a "#Form_ItemEditForm_action_doSave[readonly]" element
120+
121+
Scenario: All GridFields are reloaded after activating sudo mode via the GridField
122+
When I go to "/admin/pages"
123+
And I click on "My page" in the tree
124+
Then I should see a "#Form_EditForm_HasManyMembers .sudo-mode-password-field" element
125+
And I should see a "#Form_EditForm_ManyManyMembers .sudo-mode-password-field" element
126+
And I should not see a "#Form_EditForm_HasManyMembers .new-link" element
127+
And I should not see a "#Form_EditForm_ManyManyMembers .new-link" element
128+
When I click on the "#Form_EditForm_HasManyMembers .sudo-mode-password-field__expander-button" element
129+
And I fill in "SudoModePassword" with "Secret!123"
130+
And I click on the "#Form_EditForm_HasManyMembers .sudo-mode-password-field__verify-button" element
131+
And I wait for 2 seconds
132+
Then I should not see a "#Form_EditForm_HasManyMembers .sudo-mode-password-field" element
133+
And I should not see a "#Form_EditForm_ManyManyMembers .sudo-mode-password-field" element
134+
And I should see a "#Form_EditForm_HasManyMembers .new-link" element
135+
And I should see a "#Form_EditForm_ManyManyMembers .new-link" element
136+

0 commit comments

Comments
 (0)