ui: Ensures nested policy forms are reset properly (#5838)

1. All {{ivy-codemirror}} components need 'refreshing' when they become
visible via our own `didAppear` method on the `{{code-editor}}`
component

(also see:)
- https://github.com/hashicorp/consul/pull/4190#discussion_r193270223
- 73db111db8 (r225264296)

2. On initial investigation, it looks like the component we are using
for the code editor doesn't distinguish between setting its `value`
programatically and a `keyup` event, i.e. an interaction from the user.
We currently pretend that whenever its `value` changes, it is a `keyup`
event. This means that when we reset the `value` to `""`
programmatically for form resetting purposes, a 'pretend keyup' event
would also be fired, which would in turn kick off the validation, which
would fail and show an error message for empty values in other fields of
the form - something that is perfectly valid if you haven't typed
anything yet. We solved this by checking for `isPristine` on fields that
are allowed to be empty before you have typed anything.
This commit is contained in:
John Cowen 2019-06-04 15:57:35 +01:00 committed by GitHub
parent 9999ccf503
commit 334e16a6cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 88 additions and 9 deletions

View File

@ -61,6 +61,9 @@ export default ChildSelectorComponent.extend({
} }
}, },
actions: { actions: {
open: function(e) {
this.refreshCodeEditor(e, e.target.parentElement);
},
loadItem: function(e, item, items) { loadItem: function(e, item, items) {
const target = e.target; const target = e.target;
// the Details expander toggle, only load on opening // the Details expander toggle, only load on opening

View File

@ -1,25 +1,25 @@
import ChildSelectorComponent from './child-selector'; import ChildSelectorComponent from './child-selector';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { get, set } from '@ember/object'; import { get, set } from '@ember/object';
import { alias } from '@ember/object/computed'; import { alias } from '@ember/object/computed';
import { CallableEventSource as EventSource } from 'consul-ui/utils/dom/event-source'; import { CallableEventSource as EventSource } from 'consul-ui/utils/dom/event-source';
export default ChildSelectorComponent.extend({ export default ChildSelectorComponent.extend({
repo: service('repository/role/component'), repo: service('repository/role/component'),
dom: service('dom'),
name: 'role', name: 'role',
type: 'role', type: 'role',
classNames: ['role-selector'], classNames: ['role-selector'],
state: 'role', state: 'role',
// You have to alias data.
// If you just set it, it loses its reference?
policy: alias('policyForm.data'),
init: function() { init: function() {
this._super(...arguments); this._super(...arguments);
this.policyForm = get(this, 'formContainer').form('policy'); this.policyForm = get(this, 'formContainer').form('policy');
this.source = new EventSource(); this.source = new EventSource();
}, },
// You have to alias data
// is you just set it it loses its reference?
policy: alias('policyForm.data'),
actions: { actions: {
reset: function(e) { reset: function(e) {
this._super(...arguments); this._super(...arguments);
@ -30,9 +30,15 @@ export default ChildSelectorComponent.extend({
}, },
change: function() { change: function() {
const event = get(this, 'dom').normalizeEvent(...arguments); const event = get(this, 'dom').normalizeEvent(...arguments);
switch (event.target.name) { const target = event.target;
switch (target.name) {
case 'role[state]': case 'role[state]':
set(this, 'state', event.target.value); set(this, 'state', target.value);
if (target.value === 'policy') {
get(this, 'dom')
.component('.code-editor', target.nextElementSibling)
.didAppear();
}
break; break;
default: default:
this._super(...arguments); this._super(...arguments);

View File

@ -18,13 +18,13 @@
{{/each}} {{/each}}
</div> </div>
{{/yield-slot}} {{/yield-slot}}
<label class="type-text{{if item.error.Name ' has-error'}}"> <label class="type-text{{if (and item.error.Name (not item.isPristine)) ' has-error'}}">
<span>Name</span> <span>Name</span>
<input type="text" value={{item.Name}} name="{{name}}[Name]" autofocus="autofocus" oninput={{action 'change'}} /> <input type="text" value={{item.Name}} name="{{name}}[Name]" autofocus="autofocus" oninput={{action 'change'}} />
<em> <em>
Maximum 128 characters. May only include letters (uppercase and/or lowercase) and/or numbers. Must be unique. Maximum 128 characters. May only include letters (uppercase and/or lowercase) and/or numbers. Must be unique.
</em> </em>
{{#if item.error.Name}} {{#if (and item.error.Name (not item.isPristine))}}
<strong>{{item.error.Name.validation}}</strong> <strong>{{item.error.Name.validation}}</strong>
{{/if}} {{/if}}
</label> </label>

View File

@ -12,7 +12,7 @@
</label> </label>
{{!TODO: potentially call trigger something else}} {{!TODO: potentially call trigger something else}}
{{!the modal has to go here so that if you provide a slot to trigger it doesn't get rendered}} {{!the modal has to go here so that if you provide a slot to trigger it doesn't get rendered}}
{{#modal-dialog data-test-policy-form name="new-policy-toggle"}} {{#modal-dialog data-test-policy-form onopen=(action 'open') name="new-policy-toggle"}}
{{#block-slot 'header'}} {{#block-slot 'header'}}
<h2>New Policy</h2> <h2>New Policy</h2>
{{/block-slot}} {{/block-slot}}

View File

@ -0,0 +1,37 @@
@setupApplicationTest
Feature: dc / acls / policies / as many / reset: Reset policy form
Background:
Given 1 datacenter model with the value "datacenter"
And 1 [Model] model from yaml
---
Policies: ~
ServiceIdentities: ~
---
When I visit the [Model] page for yaml
---
dc: datacenter
[Model]: key
---
Then the url should be /datacenter/acls/[Model]s/key
And I click policies.create
Scenario: Adding a new policy as a child of [Model]
Then I fill in the policies.form form with yaml
---
Name: New-Policy
Description: New Policy Description
Rules: operator {}
---
And I click cancel on the policies.form
And I click policies.create
Then I see the policies.form form with yaml
---
Name: ""
Description: ""
Rules: ""
---
Where:
-------------
| Model |
| token |
| role |
-------------

View File

@ -0,0 +1,10 @@
import steps from '../../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -8,6 +8,7 @@ import assertHttp from './steps/assertions/http';
import assertModel from './steps/assertions/model'; import assertModel from './steps/assertions/model';
import assertPage from './steps/assertions/page'; import assertPage from './steps/assertions/page';
import assertDom from './steps/assertions/dom'; import assertDom from './steps/assertions/dom';
import assertForm from './steps/assertions/form';
// const dont = `( don't| shouldn't| can't)?`; // const dont = `( don't| shouldn't| can't)?`;
@ -79,6 +80,7 @@ export default function(assert, library, pages, utils) {
assertModel(library, assert, find, getCurrentPage, pauseUntil, utils.pluralize); assertModel(library, assert, find, getCurrentPage, pauseUntil, utils.pluralize);
assertPage(library, assert, find, getCurrentPage); assertPage(library, assert, find, getCurrentPage);
assertDom(library, assert, pauseUntil, utils.find, utils.currentURL); assertDom(library, assert, pauseUntil, utils.find, utils.currentURL);
assertForm(library, assert, find, getCurrentPage);
return library.given(["I'm using a legacy token"], function(number, model, data) { return library.given(["I'm using a legacy token"], function(number, model, data) {
window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' }); window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' });

View File

@ -0,0 +1,21 @@
export default function(scenario, assert, find, currentPage) {
scenario.then('I see the $property form with yaml\n$yaml', function(property, data) {
try {
let obj;
try {
obj = find(property);
} catch (e) {
obj = currentPage();
}
return Object.keys(data).reduce(function(prev, item, i, arr) {
const name = `${obj.prefix || property}[${item}]`;
const $el = document.querySelector(`[name="${name}"]`);
const actual = $el.value;
const expected = data[item];
assert.strictEqual(actual, expected, `Expected settings to be ${expected} was ${actual}`);
}, obj);
} catch (e) {
throw e;
}
});
}