From 2dbf330d7ba7ea91f2c199daf35a1e3c698258dd Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Wed, 22 Feb 2023 15:34:58 +0000 Subject: [PATCH] feat: add user creation to users list page --- framework/core/js/src/admin/compat.ts | 2 + .../src/admin/components/CreateUserModal.tsx | 228 ++++++++++++++++++ .../js/src/admin/components/UserListPage.tsx | 62 +++-- framework/core/less/admin.less | 1 + .../core/less/admin/CreateUserModal.less | 6 + framework/core/less/admin/UsersListPage.less | 14 ++ framework/core/locale/core.yml | 10 + 7 files changed, 309 insertions(+), 14 deletions(-) create mode 100644 framework/core/js/src/admin/components/CreateUserModal.tsx create mode 100644 framework/core/less/admin/CreateUserModal.less diff --git a/framework/core/js/src/admin/compat.ts b/framework/core/js/src/admin/compat.ts index 9ff8d9b58b..0121321425 100644 --- a/framework/core/js/src/admin/compat.ts +++ b/framework/core/js/src/admin/compat.ts @@ -35,6 +35,7 @@ import EditGroupModal from './components/EditGroupModal'; import routes from './routes'; import AdminApplication from './AdminApplication'; import generateElementId from './utils/generateElementId'; +import CreateUserModal from './components/CreateUserModal'; export default Object.assign(compat, { 'utils/saveSettings': saveSettings, @@ -70,6 +71,7 @@ export default Object.assign(compat, { 'components/AdminHeader': AdminHeader, 'components/EditCustomCssModal': EditCustomCssModal, 'components/EditGroupModal': EditGroupModal, + 'components/CreateUserModal': CreateUserModal, routes: routes, AdminApplication: AdminApplication, }); diff --git a/framework/core/js/src/admin/components/CreateUserModal.tsx b/framework/core/js/src/admin/components/CreateUserModal.tsx new file mode 100644 index 0000000000..f525eb0d70 --- /dev/null +++ b/framework/core/js/src/admin/components/CreateUserModal.tsx @@ -0,0 +1,228 @@ +import app from '../../admin/app'; +import Modal, { IInternalModalAttrs } from '../../common/components/Modal'; +import Button from '../../common/components/Button'; +import extractText from '../../common/utils/extractText'; +import ItemList from '../../common/utils/ItemList'; +import Stream from '../../common/utils/Stream'; +import type Mithril from 'mithril'; +import Switch from '../../common/components/Switch'; +import Checkbox from '../../common/components/Checkbox'; + +export interface ICreateUserModalAttrs extends IInternalModalAttrs { + username?: string; + email?: string; + password?: string; + token?: string; + provided?: string[]; +} + +export type SignupBody = { + username: string; + email: string; + isEmailConfirmed: boolean; + password: string; +}; + +export default class CreateUserModal extends Modal { + /** + * The value of the username input. + */ + username!: Stream; + + /** + * The value of the email input. + */ + email!: Stream; + + /** + * The value of the password input. + */ + password!: Stream; + + /** + * Whether email confirmation is required after signing in. + */ + requireEmailConfirmation!: Stream; + + /** + * Keeps the modal open after the user is created to facilitate creating + * multiple users at once. + */ + bulkAdd!: Stream; + + oninit(vnode: Mithril.Vnode) { + super.oninit(vnode); + + this.username = Stream(''); + this.email = Stream(''); + this.password = Stream(''); + this.requireEmailConfirmation = Stream(false); + this.bulkAdd = Stream(false); + } + + className() { + return 'Modal--small CreateUserModal'; + } + + title() { + return app.translator.trans('core.admin.create_user.title'); + } + + content() { + return ( + <> +
{this.body()}
+ + ); + } + + body() { + return ( + <> +
{this.fields().toArray()}
+ + ); + } + + fields() { + const items = new ItemList(); + + const usernameLabel = extractText(app.translator.trans('core.admin.create_user.username_placeholder')); + const emailLabel = extractText(app.translator.trans('core.admin.create_user.email_placeholder')); + const emailConfirmationLabel = extractText(app.translator.trans('core.admin.create_user.email_confirmed_label')); + const passwordLabel = extractText(app.translator.trans('core.admin.create_user.password_placeholder')); + + items.add( + 'username', +
+ +
, + 100 + ); + + items.add( + 'email', +
+ +
, + 80 + ); + + items.add( + 'emailConfirmation', +
+ this.requireEmailConfirmation(checked)} + disabled={this.loading} + > + {emailConfirmationLabel} + +
, + 60 + ); + + items.add( + 'password', +
+ +
, + 40 + ); + + items.add( + 'bulkAdd', +
+ this.bulkAdd(checked)}> + {app.translator.trans('core.admin.create_user.bulk_add_label')} + +
, + 0 + ); + + items.add( + 'submit', +
+ +
, + -10 + ); + + return items; + } + + onready() { + this.$('[name=username]').trigger('select'); + } + + onsubmit(e: SubmitEvent) { + e.preventDefault(); + + this.loading = true; + + app + .request({ + url: app.forum.attribute('apiUrl') + '/users', + method: 'POST', + body: { data: { attributes: this.submitData() } }, + errorHandler: this.onerror.bind(this), + }) + .then(() => { + this.loaded(); + + if (this.bulkAdd()) { + this.resetData(); + } else { + this.hide(); + } + }); + } + + /** + * Get the data that should be submitted in the sign-up request. + */ + submitData(): SignupBody { + const data = { + username: this.username(), + email: this.email(), + isEmailConfirmed: !this.requireEmailConfirmation(), + password: this.password(), + }; + + return data; + } + + resetData() { + this.username(''); + this.email(''); + this.password(''); + } +} diff --git a/framework/core/js/src/admin/components/UserListPage.tsx b/framework/core/js/src/admin/components/UserListPage.tsx index 1ab06e69e3..47f192fe45 100644 --- a/framework/core/js/src/admin/components/UserListPage.tsx +++ b/framework/core/js/src/admin/components/UserListPage.tsx @@ -1,4 +1,4 @@ -import type Mithril from 'mithril'; +import Mithril from 'mithril'; import app from '../../admin/app'; @@ -17,6 +17,7 @@ import classList from '../../common/utils/classList'; import extractText from '../../common/utils/extractText'; import AdminPage from './AdminPage'; import { debounce } from '../../common/utils/throttleDebounce'; +import CreateUserModal from './CreateUserModal'; type ColumnData = { /** @@ -116,19 +117,7 @@ export default class UserListPage extends AdminPage { const columns = this.columns().toArray(); return [ -
- { - this.isLoadingPage = true; - this.query = (e?.target as HTMLInputElement)?.value; - this.throttledSearch(); - }} - /> -
, -

{app.translator.trans('core.admin.users.total_users', { count: this.userCount })}

, +
{this.headerItems().toArray()}
,
{ + const items = new ItemList(); + + items.add( + 'search', +
+ { + this.isLoadingPage = true; + this.query = (e?.target as HTMLInputElement)?.value; + this.throttledSearch(); + }} + /> +
, + 100 + ); + + items.add( + 'totalUsers', +

{app.translator.trans('core.admin.users.total_users', { count: this.userCount })}

, + 90 + ); + + items.add('actions',
{this.actionItems().toArray()}
, 80); + + return items; + } + + actionItems(): ItemList { + const items = new ItemList(); + + items.add( + 'createUser', + , + 100 + ); + + return items; + } + /** * Build an item list of columns to show for each user. * diff --git a/framework/core/less/admin.less b/framework/core/less/admin.less index 2b1c658d9e..b335ab52ab 100644 --- a/framework/core/less/admin.less +++ b/framework/core/less/admin.less @@ -2,6 +2,7 @@ @import "admin/AdminHeader"; @import "admin/AdminNav"; +@import "admin/CreateUserModal"; @import "admin/DashboardPage"; @import "admin/DebugWarningWidget"; @import "admin/BasicsPage"; diff --git a/framework/core/less/admin/CreateUserModal.less b/framework/core/less/admin/CreateUserModal.less new file mode 100644 index 0000000000..f589a8f14c --- /dev/null +++ b/framework/core/less/admin/CreateUserModal.less @@ -0,0 +1,6 @@ +.CreateUserModal { + &-bulkAdd { + margin-top: 32px; + margin-bottom: 24px; + } +} diff --git a/framework/core/less/admin/UsersListPage.less b/framework/core/less/admin/UsersListPage.less index 907f2d8be0..dd0cd61850 100644 --- a/framework/core/less/admin/UsersListPage.less +++ b/framework/core/less/admin/UsersListPage.less @@ -2,6 +2,20 @@ // Pad bottom of page to make nav area look less squashed padding-bottom: 24px; + &-header { + margin-bottom: 16px; + } + + &-actions { + display: flex; + align-items: center; + justify-content: space-between; + + * + * { + margin-left: 8px; + } + } + &-grid { width: 100%; position: relative; diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index c41df63665..5611543cda 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -51,6 +51,16 @@ core: welcome_banner_heading: Welcome Banner welcome_banner_text: Configure the text that displays in the banner on the All Discussions page. Use this to welcome guests to your forum. + # These translations are used in the Create User modal. + create_user: + bulk_add_label: Keep modal open after creation? + email_placeholder: => core.ref.email + email_confirmed_label: Require user to confirm this email? + password_placeholder: => core.ref.password + submit_button: Create user + title: Create new user + username_placeholder: => core.ref.username + # These translations are used in the Dashboard page. dashboard: clear_cache_button: Clear Cache