UI/Glimmerize modal & confirmation modal component (#16032)

* remove commented out import from info-table-row

* glimmerize

* update docs

* glimmerize confirmation modal

* update modal usage

* remove keyboard action

* Revert "remove keyboard action"

This reverts commit 42b7f5950b244b5a728f94a1fbb8cd836f646ae8.

* remove keyboard actions

* address comments

* update tests
This commit is contained in:
claire bontempo 2022-06-21 12:43:34 -07:00 committed by GitHub
parent 549005e4b7
commit 5cd4fe23b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 106 additions and 84 deletions

View File

@ -346,7 +346,7 @@
<Modal
@title="Rotate your root credentials?"
@onClose={{action "continueWithoutRotate"}}
@onClose={{this.continueWithoutRotate}}
@isActive={{this.showSaveModal}}
@type="info"
@showCloseButton={{false}}
@ -381,7 +381,6 @@
@confirmText={{@model.name}}
@toConfirmMsg="deleting the connection"
@onConfirm={{action "delete"}}
@testSelector="delete"
>
<p>
Deleting the connection means that any associated roles won't be able to generate credentials until the connection is

View File

@ -256,7 +256,6 @@
@confirmText={{@model.name}}
@toConfirmMsg="deleting the key"
@onConfirm={{fn this.deleteKey @model.id}}
@testSelector="delete"
>
<p>
Destroying the

View File

@ -29,7 +29,6 @@
@isActive={{this.isEditModalActive}}
@confirmText={{@model.type}}
@onConfirm={{perform this.save}}
@testSelector="mfa"
>
<p>
Editing this configuration will have an impact on all authentication types, methods, groups and entities which make use

View File

@ -1,14 +1,6 @@
<Modal
@title={{this.title}}
@onClose={{this.onClose}}
@isActive={{this.isActive}}
@type={{this.type}}
@showCloseButton={{true}}
>
<Modal @title={{@title}} @onClose={{@onClose}} @isActive={{@isActive}} @type={{this.type}} @showCloseButton={{true}}>
<section class="modal-card-body">
{{yield}}
<div class="modal-confirm-section">
<p class="has-text-weight-semibold is-size-6">
Confirm
@ -21,7 +13,7 @@
class="input has-margin-top"
autocomplete="off"
spellcheck="false"
data-test-confirmation-modal-input={{this.testSelector}}
data-test-confirmation-modal-input={{or @title true}}
/>
</div>
</section>
@ -30,12 +22,12 @@
type="button"
class="button {{this.buttonClass}}"
disabled={{not-eq this.confirmationInput this.confirmText}}
onclick={{this.onConfirm}}
data-test-confirm-button={{this.testSelector}}
{{on "click" @onConfirm}}
data-test-confirm-button={{or @title true}}
>
{{this.buttonText}}
</button>
<button type="button" class="button is-secondary" onclick={{action (mut this.isActive) false}} data-test-cancel-button>
<button type="button" class="button is-secondary" {{on "click" @onClose}} data-test-cancel-button>
Cancel
</button>
</footer>

View File

@ -1,3 +1,4 @@
import Component from '@glimmer/component';
/**
* @module ConfirmationModal
* ConfirmationModal components are used to provide an alternative to ConfirmationButton that automatically prompts the user to fill in confirmation text before they can continue with a potentially destructive action. It is built off the Modal component
@ -13,26 +14,34 @@
* />
* ```
* @param {function} onConfirm - onConfirm is the action that happens when user clicks onConfirm after filling in the confirmation block
* @param {function} onClose - specify what to do when user attempts to close modal
* @param {boolean} isActive - Controls whether the modal is "active" eg. visible or not.
* @param {string} title - Title of the modal
* @param {function} onClose - specify what to do when user attempts to close modal
* @param {string} [buttonText=Confirm] - Button text on the confirm button
* @param {string} [confirmText=Yes] - The confirmation text that the user must type before continuing
* @param {string} [toConfirmMsg=''] - Finishes the sentence "Type <confirmText> to confirm <toConfirmMsg>", default is an empty string (ex. 'secret deletion')
* @param {string} [buttonText=Confirm] - Button text on the confirm button
* @param {string} [buttonClass=is-danger] - extra class to add to confirm button (eg. "is-danger")
* @param {string} [type=warning] - Applies message-type styling to header. Override to default with empty string
* @param {string} [toConfirmMsg] - Finishes the sentence "Type YES to confirm ..."
* @param {string} [testSelector] - The unique test selector used on the input to fill in text during tests.
* @param {string} [type=warning] - The header styling based on type, passed into the message-types helper (in the Modal component).
*/
import Component from '@ember/component';
import layout from '../templates/components/confirmation-modal';
export default class ConfirmationModal extends Component {
get buttonClass() {
return this.args.buttonClass || 'is-danger';
}
export default Component.extend({
layout,
buttonClass: 'is-danger',
buttonText: 'Confirm',
confirmText: 'Yes',
type: 'warning',
toConfirmMsg: '',
testSelector: '',
});
get buttonText() {
return this.args.buttonText || 'Confirm';
}
get confirmText() {
return this.args.confirmText || 'Yes';
}
get type() {
return this.args.type || 'warning';
}
get toConfirmMsg() {
return this.args.toConfirmMsg || '';
}
}

View File

@ -2,7 +2,6 @@ import { typeOf } from '@ember/utils';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
// import layout from '../templates/components/info-table-row';
/**
* @module InfoTableRow

View File

@ -1,6 +1,6 @@
<EmberWormhole @to="modal-wormhole">
<div class="{{this.modalClass}} {{if this.isActive 'is-active'}}" aria-modal="true">
<div class="modal-background" role="button" onclick={{this.onClose}} data-test-modal-background></div>
<div class="{{this.modalClass}} {{if this.isActive 'is-active'}}" aria-modal="true" data-test-modal-div>
<div class="modal-background" role="button" {{on "click" @onClose}} data-test-modal-background></div>
<div class="modal-card">
<header class="modal-card-head">
<h2 class="modal-card-title title is-5" data-test-modal-title>
@ -12,14 +12,14 @@
data-test-modal-glyph={{this.glyph.glyph}}
/>
{{/if}}
<span>{{this.title}}</span>
<span>{{@title}}</span>
</h2>
{{#if this.showCloseButton}}
<button
type="button"
class="delete"
aria-label="close"
onclick={{this.onClose}}
{{on "click" @onClose}}
data-test-modal-close-button
></button>
{{/if}}

View File

@ -1,3 +1,5 @@
import Component from '@glimmer/component';
import { messageTypes } from 'core/helpers/message-types';
/**
* @module Modal
* Modal components are used to overlay content on top of the page. Has a darkened background,
@ -5,37 +7,44 @@
*
* @example
* ```js
* <Modal @title={'myTitle'} @showCloseButton={true} @onClose={() => {}}/>
* <Modal
* @title="Export attribution data"
* @type="info"
* @isActive={{this.showModal}}
* @showCloseButton={{true}}
* @onClose={{action (mut this.showModal) false}}
* >
* Whatever content pops up when the modal isActive!
* </Modal>
* ```
* @param {function} onClose - onClose is the action taken when someone clicks the modal background or close button (if shown).
* @callback onClose
* @param {onClose} onClose - onClose is the action taken when someone clicks the modal background or close button (if shown).
* @param {boolean} isActive=false - whether or not modal displays
* @param {string} [title] - This text shows up in the header section of the modal.
* @param {boolean} [showCloseButton=false] - controls whether the close button in the top right corner shows.
* @param {string} type=null - The header type. This comes from the message-types helper.
* @param {string} [type=null] - The header styling based on type passed into the message-types helper.
*/
import Component from '@ember/component';
import { computed } from '@ember/object';
import { messageTypes } from 'core/helpers/message-types';
import layout from '../templates/components/modal';
export default class ModalComponent extends Component {
get isActive() {
return this.args.isActive || false;
}
export default Component.extend({
layout,
title: null,
showCloseButton: false,
type: null,
glyph: computed('type', function () {
const modalType = this.type;
if (!modalType) {
return;
get showCloseButton() {
return this.args.showCloseButton || false;
}
get glyph() {
if (!this.args.type) {
return null;
}
return messageTypes([this.type]);
}),
modalClass: computed('type', function () {
const modalType = this.type;
if (!modalType) {
return messageTypes([this.args.type]);
}
get modalClass() {
if (!this.args.type) {
return 'modal';
}
return 'modal ' + messageTypes([this.type]).class;
}),
onClose: () => {},
});
return 'modal ' + messageTypes([this.args.type]).class;
}
}

View File

@ -29,7 +29,6 @@
@confirmText={{this.model.replicationModeForDisplay}}
@toConfirmMsg="demoting this cluster"
@onConfirm={{action "onSubmit" "demote" this.model.replicationAttrs.modeForUrl}}
@testSelector="demote"
>
<p class="has-bottom-margin-m">
{{#if (and (eq this.replicationMode "dr") (not this.model.performance.replicationDisabled))}}

View File

@ -33,7 +33,6 @@
"disable"
(if (eq this.model.replicationAttrs.modeForUrl "bootstrapping") this.mode this.model.replicationAttrs.modeForUrl)
}}
@testSelector="disable"
>
<p class="has-bottom-margin-m">
{{#if (and this.model.replicationAttrs.isPrimary (eq this.model.replicationMode "dr"))}}

View File

@ -386,7 +386,7 @@ module('Acceptance | secrets/database/*', function (hooks) {
assert
.dom('.modal.is-active .title')
.hasText('Delete connection?', 'Modal appears asking to confirm delete action');
await fillIn('[data-test-confirmation-modal-input="delete"]', connectionDetails.id);
await fillIn('[data-test-confirmation-modal-input="Delete connection?"]', connectionDetails.id);
await click('[data-test-confirm-button]');
assert.equal(currentURL(), `/vault/secrets/${backend}/list`, 'Redirects to connection list page');

View File

@ -1,31 +1,41 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import sinon from 'sinon';
import { fillIn, render } from '@ember/test-helpers';
import { click, fillIn, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | confirmation-modal', function (hooks) {
setupRenderingTest(hooks);
test('it renders with disabled confirmation button until input matches', async function (assert) {
let spy = sinon.spy();
this.set('onConfirm', spy);
let confirmAction = sinon.spy();
let closeAction = sinon.spy();
this.set('onConfirm', confirmAction);
this.set('onClose', closeAction);
await render(hbs`
<div id="modal-wormhole"></div>
<ConfirmationModal
@isActive={true}
@onConfirm={this.onConfirm}
@title="Confirmation Modal"
@isActive={{true}}
@onConfirm={{this.onConfirm}}
@onClose={{this.onClose}}
@buttonText="Plz Continue"
@confirmText="Destructive Thing"
@testSelector="demote"
/>
`);
assert.dom('[data-test-confirm-button]').isDisabled();
assert.dom('[data-test-modal-div]').hasAttribute('class', 'modal is-highlight is-active');
assert.dom('[data-test-confirm-button]').hasText('Plz Continue', 'Confirm button has specified value');
assert
.dom('[data-test-modal-title]')
.hasStyle({ color: 'rgb(160, 125, 2)' }, 'title exists with warning header');
await fillIn('[data-test-confirmation-modal-input="Confirmation Modal"]', 'Destructive Thing');
assert.dom('[data-test-confirm-button="Confirmation Modal"]').isNotDisabled();
await fillIn('[data-test-confirmation-modal-input="demote"]', 'Destructive Thing');
assert.dom('[data-test-confirm-button="demote"]').isNotDisabled();
await click('[data-test-cancel-button]');
assert.true(closeAction.called, 'executes passed in onClose function');
await click('[data-test-confirm-button]');
assert.true(confirmAction.called, 'executes passed in onConfirm function');
});
});

View File

@ -53,8 +53,8 @@ module('Integration | Component | mfa-method-form', function (hooks) {
await fillIn('[data-test-input="issuer"]', 'Vault');
await click('[data-test-mfa-save]');
await fillIn('[data-test-confirmation-modal-input="mfa"]', 'totp');
await click('[data-test-confirm-button="mfa"]');
await fillIn('[data-test-confirmation-modal-input="Edit totp configuration?"]', 'totp');
await click('[data-test-confirm-button="Edit totp configuration?"]');
assert.true(this.didSave, 'onSave callback triggered');
assert.equal(this.model.issuer, 'Vault', 'Issuer property set on model');
});

View File

@ -1,23 +1,29 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import sinon from 'sinon';
import { click, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | modal', function (hooks) {
setupRenderingTest(hooks);
const closeAction = sinon.spy();
hooks.beforeEach(function () {
this.set('onClose', closeAction);
});
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<Modal></Modal><div id="modal-wormhole"></div>`);
await render(
hbs`<Modal @isActive={{true}} @onClose={{this.onClose}}></Modal><div id="modal-wormhole"></div>`
);
assert.dom(this.element).hasText('', 'renders without interior content');
assert.dom('[data-test-modal-div]').hasAttribute('class', 'modal is-active');
assert.dom('[data-test-modal-close-button]').doesNotExist('does not render close modal button');
// Template block usage:
await render(hbs`
<Modal @showCloseButton={{true}}>
<Modal @isActive={{true}} @showCloseButton={{true}} @onClose={{this.onClose}} >
template block text
</Modal>
<div id="modal-wormhole"></div>
@ -26,11 +32,13 @@ module('Integration | Component | modal', function (hooks) {
assert.dom(this.element).hasText('template block text', 'renders with interior content');
assert.dom('[data-test-modal-close-button]').exists({ count: 1 }, 'renders close modal button');
assert.dom('[data-test-modal-glyph]').doesNotExist('Glyph is not rendered by default');
await click('[data-test-modal-close-button]');
assert.true(closeAction.called, 'executes passed in onConfirm function');
});
test('it adds the correct type class', async function (assert) {
await render(hbs`
<Modal @type="warning">
<Modal @type="warning" @onClose={{this.onClose}}>
template block text
</Modal>
<div id="modal-wormhole"></div>