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:
parent
9999ccf503
commit
334e16a6cf
|
@ -61,6 +61,9 @@ export default ChildSelectorComponent.extend({
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
open: function(e) {
|
||||
this.refreshCodeEditor(e, e.target.parentElement);
|
||||
},
|
||||
loadItem: function(e, item, items) {
|
||||
const target = e.target;
|
||||
// the Details expander toggle, only load on opening
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import ChildSelectorComponent from './child-selector';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get, set } from '@ember/object';
|
||||
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
import { CallableEventSource as EventSource } from 'consul-ui/utils/dom/event-source';
|
||||
|
||||
export default ChildSelectorComponent.extend({
|
||||
repo: service('repository/role/component'),
|
||||
dom: service('dom'),
|
||||
name: 'role',
|
||||
type: 'role',
|
||||
classNames: ['role-selector'],
|
||||
state: 'role',
|
||||
// You have to alias data.
|
||||
// If you just set it, it loses its reference?
|
||||
policy: alias('policyForm.data'),
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.policyForm = get(this, 'formContainer').form('policy');
|
||||
this.source = new EventSource();
|
||||
},
|
||||
// You have to alias data
|
||||
// is you just set it it loses its reference?
|
||||
policy: alias('policyForm.data'),
|
||||
actions: {
|
||||
reset: function(e) {
|
||||
this._super(...arguments);
|
||||
|
@ -30,9 +30,15 @@ export default ChildSelectorComponent.extend({
|
|||
},
|
||||
change: function() {
|
||||
const event = get(this, 'dom').normalizeEvent(...arguments);
|
||||
switch (event.target.name) {
|
||||
const target = event.target;
|
||||
switch (target.name) {
|
||||
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;
|
||||
default:
|
||||
this._super(...arguments);
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
{{/each}}
|
||||
</div>
|
||||
{{/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>
|
||||
<input type="text" value={{item.Name}} name="{{name}}[Name]" autofocus="autofocus" oninput={{action 'change'}} />
|
||||
<em>
|
||||
Maximum 128 characters. May only include letters (uppercase and/or lowercase) and/or numbers. Must be unique.
|
||||
</em>
|
||||
{{#if item.error.Name}}
|
||||
{{#if (and item.error.Name (not item.isPristine))}}
|
||||
<strong>{{item.error.Name.validation}}</strong>
|
||||
{{/if}}
|
||||
</label>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</label>
|
||||
{{!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}}
|
||||
{{#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'}}
|
||||
<h2>New Policy</h2>
|
||||
{{/block-slot}}
|
||||
|
|
|
@ -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 |
|
||||
-------------
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -8,6 +8,7 @@ import assertHttp from './steps/assertions/http';
|
|||
import assertModel from './steps/assertions/model';
|
||||
import assertPage from './steps/assertions/page';
|
||||
import assertDom from './steps/assertions/dom';
|
||||
import assertForm from './steps/assertions/form';
|
||||
|
||||
// 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);
|
||||
assertPage(library, assert, find, getCurrentPage);
|
||||
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) {
|
||||
window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' });
|
||||
|
|
21
ui-v2/tests/steps/assertions/form.js
Normal file
21
ui-v2/tests/steps/assertions/form.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue