feat: 🎸 Add discard modal for edit grants page (#3324)

This commit is contained in:
Zhihe Li authored and GitHub committed 2026-06-18 13:35:49 -04:00
1 parent 0dac4ccf47
commit 347107c109
4 files changed
+104 -9

No files matched your search

@@ -44,7 +44,7 @@
@color='secondary' @color='secondary'
{{on 'click' @cancel}} {{on 'click' @cancel}}
/> />
{{#if this.grantStringsText}} {{#if this.grantStrings.length}}
<Hds::Button <Hds::Button
@text={{t 'actions.export'}} @text={{t 'actions.export'}}
@color='tertiary' @color='tertiary'
@@ -37,7 +37,6 @@ export default class FormRoleEditGrantsComponent extends Component {
exportOptionsMap = { terraform: 'terraform', nativeHCL: 'native-hcl' }; exportOptionsMap = { terraform: 'terraform', nativeHCL: 'native-hcl' };
exportOptions = Object.values(this.exportOptionsMap); exportOptions = Object.values(this.exportOptionsMap);
@tracked grantStringsText = (this.args.model?.grant_strings ?? []).join('\n');
@tracked currentLineText = this.args.model?.grant_strings?.[0] ?? ''; @tracked currentLineText = this.args.model?.grant_strings?.[0] ?? '';
@tracked showExportOptionsFlyout = false; @tracked showExportOptionsFlyout = false;
@tracked selectedExportOption = this.exportOptions[0]; @tracked selectedExportOption = this.exportOptions[0];
@@ -182,7 +181,11 @@ export default class FormRoleEditGrantsComponent extends Component {
const line = update.state.doc.lineAt(update.state.selection.main.head); const line = update.state.doc.lineAt(update.state.selection.main.head);
this.currentLineText = line.text; this.currentLineText = line.text;
if (update.docChanged) { if (update.docChanged) {
this.grantStringsText = update.state.doc.toString(); const text = update.state.doc.toString();
this.args.model.grant_strings = text
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
} }
// Trigger autocomplete when we're on an empty line // Trigger autocomplete when we're on an empty line
if (line.text === '') { if (line.text === '') {
@@ -200,11 +203,12 @@ export default class FormRoleEditGrantsComponent extends Component {
this.#editorView?.destroy(); this.#editorView?.destroy();
} }
get grantStringsText() {
return this.grantStrings.join('\n');
}
get grantStrings() { get grantStrings() {
return this.grantStringsText return this.args.model?.grant_strings ?? [];
.split('\n')
.map((grantString) => grantString.trim())
.filter(Boolean);
} }
/** /**
@@ -224,7 +228,7 @@ export default class FormRoleEditGrantsComponent extends Component {
*/ */
get terraformFormattedExport() { get terraformFormattedExport() {
let formatted = `grant_strings = [ \n`; let formatted = `grant_strings = [ \n`;
this.grantStringsText.split('\n').forEach((line) => { this.grantStrings.forEach((line) => {
formatted += ` "${line}",\n`; formatted += ` "${line}",\n`;
}); });
formatted += `]\n`; formatted += `]\n`;
@@ -237,7 +241,7 @@ export default class FormRoleEditGrantsComponent extends Component {
*/ */
get nativeHclFormattedExport() { get nativeHclFormattedExport() {
let formatted = `[ \n`; let formatted = `[ \n`;
this.grantStringsText.split('\n').forEach((line) => { this.grantStrings.forEach((line) => {
formatted += ` "${line}",\n`; formatted += ` "${line}",\n`;
}); });
formatted += `]\n`; formatted += `]\n`;
@@ -4,6 +4,7 @@
*/ */
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { dropTask } from 'ember-concurrency'; import { dropTask } from 'ember-concurrency';
import { modelMapping } from 'api/services/sqlite'; import { modelMapping } from 'api/services/sqlite';
@@ -60,4 +61,15 @@ export default class ScopesScopeRolesRoleEditGrantsRoute extends Route {
return response.json(); return response.json();
} }
/**
* Stores the role model in the transition data property so that the application level hook
* can check for dirty attributes and trigger the confirm service.
* @param {object} transition
*/
@action
async willTransition(transition) {
const { role } = transition.from.attributes;
transition.data = { model: role };
}
} }
@@ -348,4 +348,83 @@ module('Acceptance | roles/edit grants', function (hooks) {
assert.dom(selectors.AUTOCOMPLETE_TOOLTIP).isVisible(); assert.dom(selectors.AUTOCOMPLETE_TOOLTIP).isVisible();
}); });
test('can discard unsaved grant changes via dialog', async function (assert) {
setRunOptions({
rules: {
'color-contrast': {
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-06-17
enabled: false,
},
},
});
assert.expect(3);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.editGrants);
await waitFor(commonSelectors.CODE_EDITOR_CM);
const newGrantString = 'ids=*;type=session;actions=list';
const editorElement = find(commonSelectors.CODE_EDITOR_CODE);
const editorView = editorElement.editor;
editorView.dispatch({
changes: {
from: editorView.state.doc.length,
insert: `\n${newGrantString}`,
},
});
await click(commonSelectors.HREF(urls.role));
assert.dom(commonSelectors.MODAL_WARNING).isVisible();
await click(commonSelectors.MODAL_WARNING_CONFIRM_BTN);
assert.strictEqual(currentURL(), urls.role);
console.log(this.server.schema.roles.find(instances.role.id));
assert.false(
this.server.schema.roles
.find(instances.role.id)
.grant_strings.includes(newGrantString),
);
});
test('can cancel discard grant changes via dialog', async function (assert) {
setRunOptions({
rules: {
'color-contrast': {
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-06-17
enabled: false,
},
},
});
assert.expect(3);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.editGrants);
await waitFor(commonSelectors.CODE_EDITOR_CM);
const newGrantString = 'ids=*;type=session;actions=list';
const editorElement = find(commonSelectors.CODE_EDITOR_CODE);
const editorView = editorElement.editor;
editorView.dispatch({
changes: {
from: editorView.state.doc.length,
insert: `\n${newGrantString}`,
},
});
await click(commonSelectors.HREF(urls.role));
assert.dom(commonSelectors.MODAL_WARNING).isVisible();
await click(commonSelectors.MODAL_WARNING_CANCEL_BTN);
assert.strictEqual(currentURL(), urls.editGrants);
assert
.dom(commonSelectors.CODE_EDITOR_CONTENT)
.includesText(newGrantString);
});
}); });