Skip to content

Commit

Permalink
feat: add user creation to users list page
Browse files Browse the repository at this point in the history
  • Loading branch information
davwheat committed Feb 23, 2023
1 parent 79a9b23 commit 2dbf330
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 14 deletions.
2 changes: 2 additions & 0 deletions framework/core/js/src/admin/compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
});
228 changes: 228 additions & 0 deletions framework/core/js/src/admin/components/CreateUserModal.tsx
Original file line number Diff line number Diff line change
@@ -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<CustomAttrs extends ICreateUserModalAttrs = ICreateUserModalAttrs> extends Modal<CustomAttrs> {
/**
* The value of the username input.
*/
username!: Stream<string>;

/**
* The value of the email input.
*/
email!: Stream<string>;

/**
* The value of the password input.
*/
password!: Stream<string>;

/**
* Whether email confirmation is required after signing in.
*/
requireEmailConfirmation!: Stream<boolean>;

/**
* Keeps the modal open after the user is created to facilitate creating
* multiple users at once.
*/
bulkAdd!: Stream<boolean>;

oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
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 (
<>
<div className="Modal-body">{this.body()}</div>
</>
);
}

body() {
return (
<>
<div className="Form Form--centered">{this.fields().toArray()}</div>
</>
);
}

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',
<div className="Form-group">
<input
className="FormControl"
name="username"
type="text"
placeholder={usernameLabel}
aria-label={usernameLabel}
bidi={this.username}
disabled={this.loading}
/>
</div>,
100
);

items.add(
'email',
<div className="Form-group">
<input
className="FormControl"
name="email"
type="email"
placeholder={emailLabel}
aria-label={emailLabel}
bidi={this.email}
disabled={this.loading}
/>
</div>,
80
);

items.add(
'emailConfirmation',
<div className="Form-group">
<Switch
name="emailConfirmed"
state={this.requireEmailConfirmation()}
onchange={(checked: boolean) => this.requireEmailConfirmation(checked)}
disabled={this.loading}
>
{emailConfirmationLabel}
</Switch>
</div>,
60
);

items.add(
'password',
<div className="Form-group">
<input
className="FormControl"
name="password"
type="password"
autocomplete="new-password"
placeholder={passwordLabel}
aria-label={passwordLabel}
bidi={this.password}
disabled={this.loading}
/>
</div>,
40
);

items.add(
'bulkAdd',
<div className="Form-group CreateUserModal-bulkAdd">
<Switch name="bulkAdd" disabled={this.loading} state={this.bulkAdd()} onchange={(checked: boolean) => this.bulkAdd(checked)}>
{app.translator.trans('core.admin.create_user.bulk_add_label')}
</Switch>
</div>,
0
);

items.add(
'submit',
<div className="Form-group">
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
{app.translator.trans('core.admin.create_user.submit_button')}
</Button>
</div>,
-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('');
}
}
62 changes: 48 additions & 14 deletions framework/core/js/src/admin/components/UserListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type Mithril from 'mithril';
import Mithril from 'mithril';

import app from '../../admin/app';

Expand All @@ -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 = {
/**
Expand Down Expand Up @@ -116,19 +117,7 @@ export default class UserListPage extends AdminPage {
const columns = this.columns().toArray();

return [
<div className="Search-input">
<input
className="FormControl SearchBar"
type="search"
placeholder={app.translator.trans('core.admin.users.search_placeholder')}
oninput={(e: InputEvent) => {
this.isLoadingPage = true;
this.query = (e?.target as HTMLInputElement)?.value;
this.throttledSearch();
}}
/>
</div>,
<p class="UserListPage-totalUsers">{app.translator.trans('core.admin.users.total_users', { count: this.userCount })}</p>,
<div className="UserListPage-header">{this.headerItems().toArray()}</div>,
<section
class={classList(['UserListPage-grid', this.isLoadingPage ? 'UserListPage-grid--loadingPage' : 'UserListPage-grid--loaded'])}
style={{ '--columns': columns.length }}
Expand Down Expand Up @@ -243,6 +232,51 @@ export default class UserListPage extends AdminPage {
];
}

headerItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();

items.add(
'search',
<div className="Search-input">
<input
className="FormControl SearchBar"
type="search"
placeholder={app.translator.trans('core.admin.users.search_placeholder')}
oninput={(e: InputEvent) => {
this.isLoadingPage = true;
this.query = (e?.target as HTMLInputElement)?.value;
this.throttledSearch();
}}
/>
</div>,
100
);

items.add(
'totalUsers',
<p class="UserListPage-totalUsers">{app.translator.trans('core.admin.users.total_users', { count: this.userCount })}</p>,
90
);

items.add('actions', <div className="UserListPage-actions">{this.actionItems().toArray()}</div>, 80);

return items;
}

actionItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();

items.add(
'createUser',
<Button className="Button UserListPage-createUserBtn" icon="fas fa-user-plus" onclick={() => app.modal.show(CreateUserModal)}>
{app.translator.trans('core.admin.users.create_user_button')}
</Button>,
100
);

return items;
}

/**
* Build an item list of columns to show for each user.
*
Expand Down
1 change: 1 addition & 0 deletions framework/core/less/admin.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

@import "admin/AdminHeader";
@import "admin/AdminNav";
@import "admin/CreateUserModal";
@import "admin/DashboardPage";
@import "admin/DebugWarningWidget";
@import "admin/BasicsPage";
Expand Down
6 changes: 6 additions & 0 deletions framework/core/less/admin/CreateUserModal.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.CreateUserModal {
&-bulkAdd {
margin-top: 32px;
margin-bottom: 24px;
}
}
14 changes: 14 additions & 0 deletions framework/core/less/admin/UsersListPage.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions framework/core/locale/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2dbf330

Please sign in to comment.