mirror of
https://github.com/flarum/framework.git
synced 2025-02-21 07:38:08 +08:00
feat: add user creation to users list page (#3744)
This commit is contained in:
parent
9363682e1c
commit
21b483625e
@ -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,
|
||||
});
|
||||
|
248
framework/core/js/src/admin/components/CreateUserModal.tsx
Normal file
248
framework/core/js/src/admin/components/CreateUserModal.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
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 { generateRandomString } from '../../common/utils/string';
|
||||
|
||||
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 | null>;
|
||||
|
||||
/**
|
||||
* 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<string | null>('');
|
||||
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 useRandomPasswordLabel = extractText(app.translator.trans('core.admin.create_user.use_random_password'));
|
||||
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(
|
||||
'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 || this.password() === null}
|
||||
/>
|
||||
</div>,
|
||||
60
|
||||
);
|
||||
|
||||
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>,
|
||||
40
|
||||
);
|
||||
|
||||
items.add(
|
||||
'useRandomPassword',
|
||||
<div className="Form-group">
|
||||
<Switch
|
||||
name="useRandomPassword"
|
||||
state={this.password() === null}
|
||||
onchange={(enabled: boolean) => {
|
||||
this.password(enabled ? null : '');
|
||||
}}
|
||||
disabled={this.loading}
|
||||
>
|
||||
{useRandomPasswordLabel}
|
||||
</Switch>
|
||||
</div>,
|
||||
20
|
||||
);
|
||||
|
||||
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>,
|
||||
0
|
||||
);
|
||||
|
||||
items.add(
|
||||
'submitAndAdd',
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--block" onclick={() => this.bulkAdd(true) && this.onsubmit()} disabled={this.loading}>
|
||||
{app.translator.trans('core.admin.create_user.submit_and_create_another_button')}
|
||||
</Button>
|
||||
</div>,
|
||||
-20
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
onready() {
|
||||
this.$('[name=username]').trigger('select');
|
||||
}
|
||||
|
||||
onsubmit(e: SubmitEvent | null = null) {
|
||||
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(() => {
|
||||
if (this.bulkAdd()) {
|
||||
this.resetData();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.bulkAdd(false);
|
||||
this.loaded();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() ?? generateRandomString(32),
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
resetData() {
|
||||
this.username('');
|
||||
this.email('');
|
||||
this.password('');
|
||||
}
|
||||
}
|
@ -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 [
|
||||
<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 className="UserListPage-totalUsers">{app.translator.trans('core.admin.users.total_users', { count: this.userCount })}</p>,
|
||||
<div className="UserListPage-header">{this.headerItems().toArray()}</div>,
|
||||
<section
|
||||
className={classList(['UserListPage-grid', this.isLoadingPage ? 'UserListPage-grid--loadingPage' : 'UserListPage-grid--loaded'])}
|
||||
style={{ '--columns': columns.length }}
|
||||
@ -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.
|
||||
*
|
||||
|
@ -79,3 +79,23 @@ export function ucfirst(string: string): string {
|
||||
export function camelCaseToSnakeCase(str: string): string {
|
||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string (a-z, 0-9) of a given length.
|
||||
*
|
||||
* Providing a length of less than 0 will result in an error.
|
||||
*
|
||||
* @param length Length of the random string to generate
|
||||
* @returns A random string of provided length
|
||||
*/
|
||||
export function generateRandomString(length: number): string {
|
||||
if (length < 0) throw new Error('Cannot generate a random string with length less than 0.');
|
||||
if (length === 0) return '';
|
||||
|
||||
const arr = new Uint8Array(length / 2);
|
||||
window.crypto.getRandomValues(arr);
|
||||
|
||||
return Array.from(arr, (dec) => {
|
||||
return dec.toString(16).padStart(2, '0');
|
||||
}).join('');
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
@import "admin/AdminHeader";
|
||||
@import "admin/AdminNav";
|
||||
@import "admin/CreateUserModal";
|
||||
@import "admin/DashboardPage";
|
||||
@import "admin/DebugWarningWidget";
|
||||
@import "admin/BasicsPage";
|
||||
|
6
framework/core/less/admin/CreateUserModal.less
Normal file
6
framework/core/less/admin/CreateUserModal.less
Normal file
@ -0,0 +1,6 @@
|
||||
.CreateUserModal {
|
||||
&-bulkAdd {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -52,6 +52,17 @@ 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:
|
||||
email_placeholder: => core.ref.email
|
||||
email_confirmed_label: Require user to confirm this email
|
||||
password_placeholder: => core.ref.password
|
||||
submit_and_create_another_button: Create and add another
|
||||
submit_button: Create user
|
||||
title: Create new user
|
||||
use_random_password: Generate random password
|
||||
username_placeholder: => core.ref.username
|
||||
|
||||
# These translations are used in the Dashboard page.
|
||||
dashboard:
|
||||
clear_cache_button: Clear Cache
|
||||
@ -250,6 +261,7 @@ core:
|
||||
|
||||
# These translations are used for the users list on the admin dashboard.
|
||||
users:
|
||||
create_user_button: New User
|
||||
description: A paginated list of all users on your forum.
|
||||
|
||||
grid:
|
||||
@ -785,7 +797,7 @@ core:
|
||||
all_discussions: All Discussions
|
||||
change_email: Change Email
|
||||
change_password: Change Password
|
||||
color: Color # Referenced by flarum-tags.yml
|
||||
color: Color # Referenced by flarum-tags.yml
|
||||
confirm_password: Confirm Password
|
||||
confirm_email: Confirm Email
|
||||
confirmation_email_sent: "We've sent a confirmation email to {email}. If it doesn't arrive soon, check your spam folder."
|
||||
@ -795,7 +807,7 @@ core:
|
||||
custom_header_title: Edit Custom Header
|
||||
delete: Delete
|
||||
delete_forever: Delete Forever
|
||||
discussions: Discussions # Referenced by flarum-statistics.yml
|
||||
discussions: Discussions # Referenced by flarum-statistics.yml
|
||||
edit: Edit
|
||||
edit_user: Edit User
|
||||
email: Email
|
||||
@ -812,27 +824,27 @@ core:
|
||||
new_token: New Token
|
||||
next_page: Next Page
|
||||
notifications: Notifications
|
||||
okay: OK # Referenced by flarum-tags.yml
|
||||
okay: OK # Referenced by flarum-tags.yml
|
||||
password: Password
|
||||
posts: Posts # Referenced by flarum-statistics.yml
|
||||
posts: Posts # Referenced by flarum-statistics.yml
|
||||
previous_page: Previous Page
|
||||
remove: Remove
|
||||
rename: Rename
|
||||
reply: Reply # Referenced by flarum-mentions.yml
|
||||
reply: Reply # Referenced by flarum-mentions.yml
|
||||
reset_your_password: Reset Your Password
|
||||
restore: Restore
|
||||
save_changes: Save Changes
|
||||
search_users: Search users # Referenced by flarum-suspend.yml, flarum-tags.yml
|
||||
search_users: Search users # Referenced by flarum-suspend.yml, flarum-tags.yml
|
||||
security: Security
|
||||
settings: Settings
|
||||
sign_up: Sign Up
|
||||
some_others: "{count, plural, one {# other} other {# others}}" # Referenced by flarum-likes.yml, flarum-mentions.yml
|
||||
some_others: "{count, plural, one {# other} other {# others}}" # Referenced by flarum-likes.yml, flarum-mentions.yml
|
||||
start_a_discussion: Start a Discussion
|
||||
username: Username
|
||||
users: Users # Referenced by flarum-statistics.yml
|
||||
users: Users # Referenced by flarum-statistics.yml
|
||||
view: View
|
||||
write_a_reply: Write a Reply...
|
||||
you: You # Referenced by flarum-likes.yml, flarum-mentions.yml
|
||||
you: You # Referenced by flarum-likes.yml, flarum-mentions.yml
|
||||
|
||||
##
|
||||
# GROUP NAMES - These keys are translated at the back end.
|
||||
|
Loading…
x
Reference in New Issue
Block a user