UX: improve layout and styles for solo preferences (#21094)

This commit is contained in:
Kris 2023-04-19 09:41:02 -04:00 committed by GitHub
parent 1ee87cbfa3
commit 4eb7d2d79b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 389 additions and 287 deletions

View File

@ -6,23 +6,25 @@
readonly
>{{this.formattedBackupCodes}}</textarea>
<DButton
@action={{action "copyToClipboard"}}
@class="backup-codes-copy-btn"
@icon="copy"
@aria-label="user.second_factor_backup.copy_to_clipboard"
@title="user.second_factor_backup.copy_to_clipboard"
/>
<div class="controls">
<DButton
@action={{action "copyToClipboard"}}
@class="btn-default backup-codes-copy-btn"
@icon="copy"
@aria-label="user.second_factor_backup.copy_to_clipboard"
@title="user.second_factor_backup.copy_to_clipboard"
/>
<a
download="{{this.siteTitleSlug}}-backup-codes.txt"
class="btn no-text btn-icon backup-codes-download-btn"
aria-label={{i18n "user.second_factor_backup.download_backup_codes"}}
title={{i18n "user.second_factor_backup.download_backup_codes"}}
rel="noopener noreferrer"
target="_blank"
href="data:application/octet-stream;charset=utf-8;base64,{{this.base64BackupCode}}"
>
{{d-icon "download"}}
</a>
<a
download="{{this.siteTitleSlug}}-backup-codes.txt"
class="btn btn-default no-text btn-icon backup-codes-download-btn"
aria-label={{i18n "user.second_factor_backup.download_backup_codes"}}
title={{i18n "user.second_factor_backup.download_backup_codes"}}
rel="noopener noreferrer"
target="_blank"
href="data:application/octet-stream;charset=utf-8;base64,{{this.base64BackupCode}}"
>
{{d-icon "download"}}
</a>
</div>
</div>

View File

@ -0,0 +1,43 @@
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
import I18n from "I18n";
import { computed } from "@ember/object";
export default DropdownSelectBoxComponent.extend({
classNames: ["security-key-dropdown"],
selectKitOptions: {
icon: "wrench",
showFullTitle: false,
},
content: computed(function () {
const content = [];
content.push({
id: "edit",
icon: "pencil-alt",
name: I18n.t("user.second_factor.edit"),
});
content.push({
id: "disable",
icon: "trash-alt",
name: I18n.t("user.second_factor.disable"),
});
return content;
}),
actions: {
onChange(id) {
switch (id) {
case "edit":
this.editSecurityKey(this.securityKey);
break;
case "disable":
this.disableSingleSecondFactor(this.securityKey);
break;
}
},
},
});

View File

@ -0,0 +1,40 @@
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
import I18n from "I18n";
import { computed } from "@ember/object";
export default DropdownSelectBoxComponent.extend({
classNames: ["token-based-auth-dropdown"],
selectKitOptions: {
icon: "wrench",
showFullTitle: false,
},
content: computed(function () {
return [
{
id: "edit",
icon: "pencil-alt",
name: I18n.t("user.second_factor.edit"),
},
{
id: "disable",
icon: "trash-alt",
name: I18n.t("user.second_factor.disable"),
},
];
}),
actions: {
onChange(id) {
switch (id) {
case "edit":
this.editSecondFactor(this.totp);
break;
case "disable":
this.disableSingleSecondFactor(this.totp);
break;
}
},
},
});

View File

@ -0,0 +1,45 @@
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
import I18n from "I18n";
import { computed } from "@ember/object";
export default DropdownSelectBoxComponent.extend({
classNames: ["two-factor-backup-dropdown"],
selectKitOptions: {
icon: "wrench",
showFullTitle: false,
},
content: computed(function () {
const content = [];
content.push({
id: "edit",
icon: "pencil-alt",
name: I18n.t("user.second_factor.edit"),
});
if (this.secondFactorBackupEnabled) {
content.push({
id: "disable",
icon: "trash-alt",
name: I18n.t("user.second_factor.disable"),
});
}
return content;
}),
actions: {
onChange(id) {
switch (id) {
case "edit":
this.editSecondFactorBackup();
break;
case "disable":
this.disableSecondFactorBackup();
break;
}
},
},
});

View File

@ -1,64 +1,61 @@
<DModalBody>
<section
class="user-content user-preferences solo-preference second-factor-backup-preferences"
>
<form class="form-horizontal">
{{#if this.successMessage}}
<div class="alert alert-success">
{{this.successMessage}}
</div>
{{/if}}
{{#if this.successMessage}}
<div class="alert alert-success">
{{this.successMessage}}
</div>
{{/if}}
{{#if this.errorMessage}}
<div class="alert alert-error">
{{this.errorMessage}}
</div>
{{/if}}
{{#if this.errorMessage}}
<div class="alert alert-error">
{{this.errorMessage}}
</div>
{{/if}}
{{#if this.backupEnabled}}
{{html-safe
(i18n
"user.second_factor_backup.remaining_codes"
count=this.remainingCodes
)
}}
{{/if}}
<ConditionalLoadingSection @isLoading={{this.loading}}>
{{#if this.backupCodes}}
<h3>{{i18n "user.second_factor_backup.codes.title"}}</h3>
<div class="actions">
{{#if this.backupEnabled}}
<DButton
@class="btn-primary"
@icon="redo"
@action={{action "generateSecondFactorCodes"}}
@type="submit"
@isLoading={{this.loading}}
@label="user.second_factor_backup.regenerate"
/>
{{else}}
<DButton
@class="btn-primary"
@action={{action "generateSecondFactorCodes"}}
@type="submit"
@disabled={{this.loading}}
@label="user.second_factor_backup.enable"
/>
{{/if}}
</div>
<p>
{{i18n "user.second_factor_backup.codes.description"}}
</p>
<ConditionalLoadingSection @isLoading={{this.loading}}>
{{#if this.backupCodes}}
<h3>{{i18n "user.second_factor_backup.codes.title"}}</h3>
<BackupCodes
@copyBackupCode={{action "copyBackupCode"}}
@backupCodes={{this.backupCodes}}
/>
{{/if}}
</ConditionalLoadingSection>
<p>
{{i18n "user.second_factor_backup.codes.description"}}
</p>
{{#if this.backupEnabled}}
{{html-safe
(i18n
"user.second_factor_backup.remaining_codes" count=this.remainingCodes
)
}}
{{else}}
{{html-safe (i18n "user.second_factor_backup.not_enabled")}}
{{/if}}
</DModalBody>
<BackupCodes
@copyBackupCode={{action "copyBackupCode"}}
@backupCodes={{this.backupCodes}}
/>
{{/if}}
</ConditionalLoadingSection>
</form>
</section>
</DModalBody>
<div class="modal-footer">
<div class="actions">
{{#if this.backupEnabled}}
<DButton
@class="btn-primary"
@icon="redo"
@action={{action "generateSecondFactorCodes"}}
@type="submit"
@isLoading={{this.loading}}
@label="user.second_factor_backup.regenerate"
/>
{{else}}
<DButton
@class="btn-primary"
@action={{action "generateSecondFactorCodes"}}
@type="submit"
@disabled={{this.loading}}
@label="user.second_factor_backup.enable"
/>
{{/if}}
</div>
</div>

View File

@ -1,16 +1,10 @@
<DModalBody>
<form class="form-horizontal">
<div class="input-group">
<label for="authenticator-name">{{i18n
"user.second_factor.edit_description"
}}</label>
<Input
name="authenticator-name"
@type="text"
@value={{this.model.name}}
/>
</div>
</form>
<div class="input-group">
<label for="authenticator-name">{{i18n
"user.second_factor.edit_description"
}}</label>
<Input name="authenticator-name" @type="text" @value={{this.model.name}} />
</div>
</DModalBody>
<div class="modal-footer">

View File

@ -1,34 +1,23 @@
<DSection @pageClass="user-preferences" @tagName="">
<section class="user-content user-preferences solo-preference">
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<h3>{{i18n
(if this.new "user.add_email.title" "user.change_email.title")
}}</h3>
</div>
</div>
<form class="form-vertical">
{{#if this.success}}
<div class="control-group">
<div class="controls">
<div class="instructions">
<p>{{this.successMessage}}</p>
</div>
</div>
</div>
<div class="alert alert-success">{{this.successMessage}}</div>
<LinkTo @route="preferences.account" class="success-back">
{{d-icon "arrow-left"}}
{{i18n "user.change_email.back_to_preferences"}}
</LinkTo>
{{else}}
{{#if this.error}}
<div class="control-group">
<div class="controls">
<div class="alert alert-error">{{this.errorMessage}}</div>
</div>
</div>
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
<div class="control-group">
<label class="control-label">{{i18n "user.email.title"}}</label>
<label class="control-label">
{{i18n
(if this.new "user.add_email.title" "user.change_email.title")
}}
</label>
<div class="controls">
<TextField
@value={{this.newEmail}}
@ -36,9 +25,6 @@
@classNames="input-xxlarge"
@autofocus="autofocus"
/>
<InputTip @validation={{this.emailValidation}} />
</div>
<div class="controls">
<div class="instructions">
{{#if this.taken}}
{{i18n "user.change_email.taken"}}
@ -46,22 +32,23 @@
{{i18n "user.email.instructions"}}
{{/if}}
</div>
<InputTip @validation={{this.emailValidation}} />
</div>
</div>
<div class="control-group">
<div class="controls">
<DButton
@class="btn-primary"
@action={{action "saveEmail"}}
@type="submit"
@disabled={{this.saveDisabled}}
@translatedLabel={{this.saveButtonText}}
/>
</div>
<div class="controls save-button">
<DButton
@class="btn-primary"
@action={{action "saveEmail"}}
@type="submit"
@disabled={{this.saveDisabled}}
@translatedLabel={{this.saveButtonText}}
/>
<CancelLink
@route="preferences.account"
@args={{this.model.username}}
/>
</div>
{{/if}}
</form>
</section>
</DSection>

View File

@ -1,31 +1,21 @@
<DSection @pageClass="user-preferences" @tagName="">
<section class="user-content user-preferences solo-preference second-factor">
<ConditionalLoadingSpinner @condition={{this.loading}}>
<form class="form-horizontal">
<form class="form-vertical">
{{#if this.showEnforcedNotice}}
<div class="control-group">
<div class="controls">
<div class="alert alert-error">{{i18n
"user.second_factor.enforced_notice"
}}</div>
</div>
</div>
<div class="alert alert-error">{{i18n
"user.second_factor.enforced_notice"
}}</div>
{{/if}}
{{#if this.displayOAuthWarning}}
<div class="control-group">
<div class="controls">
{{i18n "user.second_factor.oauth_enabled_warning"}}
</div>
</div>
<div class="alert alert-warning">{{i18n
"user.second_factor.oauth_enabled_warning"
}}</div>
{{/if}}
{{#if this.errorMessage}}
<div class="control-group">
<div class="controls">
<div class="alert alert-error">{{this.errorMessage}}</div>
</div>
</div>
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
{{#if this.loaded}}
@ -41,23 +31,14 @@
{{i18n "user.second_factor.totp.default_name"}}
{{/if}}
</div>
{{#if this.isCurrentUser}}
<div class="actions">
<DButton
@action={{action "editSecondFactor" totp}}
@class="btn-default btn-flat btn-small btn-icon pad-left no-text edit"
@disabled={{this.loading}}
@icon="pencil-alt"
@aria-label="user.second_factor.edit"
@title="user.second_factor.edit"
/>
<DButton
@action={{action "disableSingleSecondFactor" totp}}
@class="btn-danger btn-flat no-text"
@icon="trash-alt"
@aria-label="user.second_factor.disable"
@title="user.second_factor.disable"
<TokenBasedAuthDropdown
@totp={{totp}}
@editSecondFactor={{action "editSecondFactor"}}
@disableSingleSecondFactor={{action
"disableSingleSecondFactor"
}}
/>
</div>
{{/if}}
@ -65,7 +46,7 @@
{{/each}}
<DButton
@action={{action "createTotp"}}
@class="btn-primary new-totp"
@class="btn-default new-totp"
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor.totp.add"
@ -88,23 +69,12 @@
{{#if this.isCurrentUser}}
<div class="actions">
<DButton
@action={{action "editSecurityKey" security_key}}
@class="btn-default btn-flat btn-small btn-icon pad-left no-text edit"
@disabled={{this.loading}}
@icon="pencil-alt"
@aria-label="user.second_factor.edit"
@title="user.second_factor.edit"
/>
<DButton
@action={{action
<SecurityKeyDropdown
@securityKey={{security_key}}
@editSecurityKey={{action "editSecurityKey"}}
@disableSingleSecondFactor={{action
"disableSingleSecondFactor"
security_key
}}
@class="btn-danger btn-flat no-text"
@icon="trash-alt"
@aria-label="user.second_factor.disable"
@title="user.second_factor.disable"
/>
</div>
{{/if}}
@ -112,7 +82,7 @@
{{/each}}
<DButton
@action={{action "createSecurityKey"}}
@class="btn-primary new-security-key"
@class="btn-default new-security-key"
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor.security_key.add"
@ -134,32 +104,34 @@
)
}}
{{else}}
{{i18n "user.second_factor_backup.enable_long"}}
<DButton
@action={{action "editSecondFactorBackup"}}
@class="btn-default new-second-factor-backup"
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor_backup.enable_long"
/>
{{/if}}
</div>
{{#if this.isCurrentUser}}
{{#if
(and
this.model.second_factor_backup_enabled this.isCurrentUser
)
}}
<div class="actions">
<DButton
@action={{action "editSecondFactorBackup"}}
@class="btn-default btn-flat btn-small btn-icon pad-left no-text edit edit-2fa-backup"
@disabled={{this.loading}}
@icon="pencil-alt"
@aria-label="user.second_factor.edit"
@title="user.second_factor.edit"
<TwoFactorBackupDropdown
@secondFactorBackupEnabled={{this.model.second_factor_backup_enabled}}
@editSecondFactorBackup={{action
"editSecondFactorBackup"
}}
@disableSecondFactorBackup={{action
"disableSecondFactorBackup"
}}
/>
{{#if this.model.second_factor_backup_enabled}}
<DButton
@action={{action "disableSecondFactorBackup"}}
@class="btn-danger btn-flat no-text"
@icon="trash-alt"
@aria-label="user.second_factor.disable"
@title="user.second_factor.disable"
/>
{{/if}}
</div>
{{/if}}
{{else}}
{{i18n "user.second_factor_backup.enable_prerequisites"}}
{{/if}}
@ -170,7 +142,7 @@
{{#if this.model.second_factor_enabled}}
{{#unless this.showEnforcedNotice}}
<div class="control-group pref-second-factor-disable-all">
<div class="controls">
<div class="controls -actions">
<DButton
@class="btn-danger"
@icon="ban"
@ -178,6 +150,10 @@
@disabled={{this.loading}}
@label="user.second_factor.disable_all"
/>
<CancelLink
@route="preferences.security"
@args={{this.model.username}}
/>
</div>
</div>
{{/unless}}

View File

@ -32,14 +32,14 @@ acceptance("User Preferences - Second Factor Backup", function (needs) {
test("second factor backup", async function (assert) {
updateCurrentUser({ second_factor_enabled: true });
await visit("/u/eviltrout/preferences/second-factor");
await click(".edit-2fa-backup");
await click(".new-second-factor-backup");
assert.ok(
exists(".second-factor-backup-preferences"),
exists(".second-factor-backup-edit-modal"),
"shows the 2fa backup panel"
);
await click(".second-factor-backup-preferences .btn-primary");
await click(".second-factor-backup-edit-modal .btn-primary");
assert.ok(exists(".backup-codes-area"), "shows backup codes");
});
@ -47,10 +47,16 @@ acceptance("User Preferences - Second Factor Backup", function (needs) {
test("delete backup codes", async function (assert) {
updateCurrentUser({ second_factor_enabled: true });
await visit("/u/eviltrout/preferences/second-factor");
await click(".edit-2fa-backup");
await click(".second-factor-backup-preferences .btn-primary");
await click(".modal-close");
await click(".pref-second-factor-backup .btn-danger");
if (exists(".new-second-factor-backup")) {
// if codes don't exist yet, create them
await click(".new-second-factor-backup");
await click(".second-factor-backup-edit-modal .btn-primary");
await click(".modal-close");
}
await click(".two-factor-backup-dropdown .select-kit-header");
await click("li[data-name='Disable'");
assert.strictEqual(
query("#dialog-title").innerText.trim(),
"Deleting backup codes"

View File

@ -108,7 +108,9 @@ acceptance("User Preferences - Second Factor", function (needs) {
await fillIn("#password", "secrets");
await click(".user-preferences .btn-primary");
await click(".totp .btn-danger");
await click(".token-based-auth-dropdown .select-kit-header");
await click("li[data-name='Disable']");
assert.strictEqual(
query("#dialog-title").innerText.trim(),
"Deleting an authenticator"
@ -120,7 +122,9 @@ acceptance("User Preferences - Second Factor", function (needs) {
"User has a physical security key"
);
await click(".security-key .btn-danger");
await click(".security-key-dropdown .select-kit-header");
await click("li[data-name='Disable'");
assert.strictEqual(
query("#dialog-title").innerText.trim(),
"Deleting an authenticator"

View File

@ -570,6 +570,7 @@ table {
display: inline-block;
margin-bottom: 0;
flex: 0 0 auto;
max-width: 100%;
}
.control-group {

View File

@ -580,59 +580,6 @@
.second-factor-token-input {
margin-right: 10px;
}
.form-horizontal {
.instructions {
margin-left: 0;
}
.actions {
margin-top: 5px;
}
}
.backup-codes {
margin: 2em 0;
.wrapper {
display: inline-block;
position: relative;
border-radius: 3px;
border: 1px solid var(--primary-low);
width: 100%;
}
.backup-codes-area {
resize: none;
padding: 0;
height: auto;
text-align: center;
width: 100%;
background: var(--secondary);
border: 0;
cursor: auto;
outline: none;
font-family: monospace;
&:focus {
box-shadow: none;
border-color: var(--primary-low);
}
}
.backup-codes-copy-btn,
.backup-codes-download-btn {
right: 5px;
position: absolute;
}
.backup-codes-copy-btn {
top: 5px;
}
.backup-codes-download-btn {
top: 40px;
}
}
}
.pref-associated-accounts table {
@ -661,9 +608,16 @@
}
.instructions {
clear: both;
display: inline-block;
margin-top: 4px;
display: block;
margin-top: 0.25em;
}
.success-back {
display: flex;
align-items: center;
.d-icon {
margin-right: 0.25em;
}
}
@mixin inactiveMode() {
@ -687,6 +641,11 @@
.undo-preview {
margin-bottom: 1em;
}
.save-button {
display: flex;
align-items: center;
}
}
.paginated-topics-list {
@ -729,30 +688,77 @@
}
.second-factor {
&.instructions {
color: var(--primary-medium);
margin-top: 5px;
margin-bottom: 10px;
font-size: var(--font-down-1);
}
.second-factor-item {
margin-top: 0.75em;
width: 500px;
width: 100%;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #ddd;
margin: 5px 0px;
border-top: 1px solid var(--primary-low);
margin: 0.25em 0;
padding: 0.25em 0;
align-items: center;
&:last-child {
border-bottom: 0;
}
.btn-danger .d-icon {
color: var(--danger);
.select-kit {
.select-kit-header {
background: transparent;
&:hover .d-icon {
color: var(--primary-high);
}
}
&.is-expanded {
.select-kit-header .d-icon {
color: var(--primary-high);
}
}
}
}
.btn.edit {
min-height: auto;
.-actions {
display: flex;
align-items: center;
}
}
.d-modal[class*="second-factor-"] {
.modal-inner-container {
max-width: 24em;
}
}
.backup-codes {
margin: 1em 0;
.wrapper {
display: flex;
border: 1px solid var(--primary-low);
width: 100%;
}
textarea.backup-codes-area {
flex: 1 1 100%;
height: 100%;
resize: none;
margin: 0;
padding: 0.5em;
height: auto;
background: var(--secondary);
border: 0;
cursor: auto;
outline: none;
font-family: monospace;
&:focus {
box-shadow: none;
border-color: var(--primary-low);
}
}
.controls {
padding: 0.5em;
flex: 1 1 2em;
margin-left: auto;
.btn {
margin-bottom: 0.5em;
}
}
}

View File

@ -156,11 +156,6 @@ input {
.controls {
margin-left: 160px;
&.-actions {
display: flex;
align-items: center;
}
}
}

View File

@ -330,6 +330,10 @@
.apps .controls button {
float: right;
}
#change-email {
width: 100%;
}
}
.user-right {

View File

@ -1419,11 +1419,12 @@ en:
title: "Two-Factor Backup Codes"
regenerate: "Regenerate"
disable: "Disable"
enable: "Enable"
enable_long: "Enable backup codes"
enable: "Create backup codes"
enable_long: "Add backup codes"
not_enabled: "You haven't created any backup codes yet."
manage:
one: "Manage backup codes. You have <strong>%{count}</strong> backup code remaining."
other: "Manage backup codes. You have <strong>%{count}</strong> backup codes remaining."
one: "You have <strong>%{count}</strong> backup code remaining."
other: "You have <strong>%{count}</strong> backup codes remaining."
copy_to_clipboard: "Copy to Clipboard"
copy_to_clipboard_error: "Error copying data to Clipboard"
copied_to_clipboard: "Copied to Clipboard"
@ -1512,6 +1513,7 @@ en:
success: "We've sent an email to that address. Please follow the confirmation instructions."
success_via_admin: "We've sent an email to that address. The user will need to follow the confirmation instructions in the email."
success_staff: "We've sent an email to your current address. Please follow the confirmation instructions."
back_to_preferences: "Back to preferences"
change_avatar:
title: "Change your profile picture"