mirror of
https://github.com/flarum/core.git
synced 2025-10-15 08:55:53 +02:00
Add users list to admin dashboard (#2626)
* Commit initial WIP code
* Fix squashed grid on mobile
* Add pagination support; rename to userList
* Improve grid sizing
* Improve grid row shading
* Move EditUserModal to common
* Add link to profile page in grid
* Use Less styling vars
* Move EditUserModal translations to lib
* Add edit user button to grid
* Fix incorrect profile link priority
* Update profile link translation key
* Add priorities to other columns
* Add group badges to grid
* Add username to profile link tooltip
* Organise imports
* Use variable for header border bottom color
* Fix broken export
* Add total user count to API payload's metadata
* Add new metadata to ApiPayload type
* Implement correct page number
* Remove debug code
* Use function to get the total pages
This allows us to use the raw count elsewhere in the component (pssst... check the next commit!)
* Center profile link in column
* Add profile link header
* Show total users above table
* Use ItemList's itemName property for column data attributes
* Add user email column, hidden by default
This column is hidden by default using a placeholder email and blur filter. These are then removed when the visibility toggle is pressed.
This prevents any over-the-shoulder accidental data leakage, as emails are classed as PII under GDPR.
* Fix incorrect tooltip translation keys
* Add extra padding between email and visibility toggle button
* Prevent selection of blurred email
* Fix incorrect icon state for email toggle
* Update API response type to include metadata (for now)
* Increase number of users per page to 50
* Update compat files with new locations
* Format
* Add @deprecated notices for forum compat export
* Use AdminPayload for user count instead of supplying as REST API metadata
* Make nav look less squashed using bottom margin
* Suppress TS warning
* StyleCI fixes
* Fix TS error
* Update based on review comments
* Rename user list -> users
* Rename internal instances of user_list to users
* Fix formatting
* Use CSS custom properties for the table column count
* Use .Button--icon instead of custom style
* Make fake email more realistic length
* Add a11y attributes
* Use padding bottom instead of margin bottom for page spacing
* Make compatible with new CSS LoadingIndicator
I won't let it break here! :P
* Integrate profile link into username column
* Don't force columns to be 300px
This made the grid look very bloated and intimidating -- lets instead increase the padding between items and make it only the width it needs to be.
* Center edit user button in column
* Increase spacing between email and visibility toggle button
* Rename `statistics` to `modelStatistics` in Admin payload
This prevents any possible conflicts with core and `flarum/statistics`. We might want to consider migrating the stats extension to extend this object in the future.
* Update comments, fix TS error
* Various translation key changes
* Change gmail.com -> example.com
* Stretch 'edit user' button to entire cell size
* Update translations
* Is the YAML formatted right this time? 🙈
* Remove email placeholder
Fixes an issue where the table would jump if an email was unhidden that was longer than the placeholder.
* Re-order lib translations
* Clicking blurred email now unblurs
* Correct header class
* Improve edit user button centring
* Improve vertical row item centering
* Fix incorrect column length in aria attribute
* Use .Button--text!
This commit is contained in:
@@ -59,6 +59,7 @@ import Modal from './components/Modal';
|
||||
import GroupBadge from './components/GroupBadge';
|
||||
import TextEditor from './components/TextEditor';
|
||||
import TextEditorButton from './components/TextEditorButton';
|
||||
import EditUserModal from './components/EditUserModal';
|
||||
import Model from './Model';
|
||||
import Application from './Application';
|
||||
import fullTime from './helpers/fullTime';
|
||||
@@ -136,6 +137,7 @@ export default {
|
||||
'components/GroupBadge': GroupBadge,
|
||||
'components/TextEditor': TextEditor,
|
||||
'components/TextEditorButton': TextEditorButton,
|
||||
'components/EditUserModal': EditUserModal,
|
||||
Model: Model,
|
||||
Application: Application,
|
||||
'helpers/fullTime': fullTime,
|
||||
|
246
js/src/common/components/EditUserModal.js
Normal file
246
js/src/common/components/EditUserModal.js
Normal file
@@ -0,0 +1,246 @@
|
||||
import Modal from './Modal';
|
||||
import Button from './Button';
|
||||
import GroupBadge from './GroupBadge';
|
||||
import Group from '../models/Group';
|
||||
import extractText from '../utils/extractText';
|
||||
import ItemList from '../utils/ItemList';
|
||||
import Stream from '../utils/Stream';
|
||||
|
||||
/**
|
||||
* The `EditUserModal` component displays a modal dialog with a login form.
|
||||
*/
|
||||
export default class EditUserModal extends Modal {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
const user = this.attrs.user;
|
||||
|
||||
this.username = Stream(user.username() || '');
|
||||
this.email = Stream(user.email() || '');
|
||||
this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false);
|
||||
this.setPassword = Stream(false);
|
||||
this.password = Stream(user.password() || '');
|
||||
this.groups = {};
|
||||
|
||||
app.store
|
||||
.all('groups')
|
||||
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.forEach((group) => (this.groups[group.id()] = Stream(user.groups().indexOf(group) !== -1)));
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'EditUserModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.translator.trans('core.lib.edit_user.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
const fields = this.fields().toArray();
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
{fields.length > 1 ? <div className="Form">{this.fields().toArray()}</div> : app.translator.trans('core.lib.edit_user.nothing_available')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fields() {
|
||||
const items = new ItemList();
|
||||
|
||||
if (app.session.user.canEditCredentials()) {
|
||||
items.add(
|
||||
'username',
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('core.lib.edit_user.username_heading')}</label>
|
||||
<input
|
||||
className="FormControl"
|
||||
placeholder={extractText(app.translator.trans('core.lib.edit_user.username_label'))}
|
||||
bidi={this.username}
|
||||
disabled={this.nonAdminEditingAdmin()}
|
||||
/>
|
||||
</div>,
|
||||
40
|
||||
);
|
||||
|
||||
if (app.session.user !== this.attrs.user) {
|
||||
items.add(
|
||||
'email',
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('core.lib.edit_user.email_heading')}</label>
|
||||
<div>
|
||||
<input
|
||||
className="FormControl"
|
||||
placeholder={extractText(app.translator.trans('core.lib.edit_user.email_label'))}
|
||||
bidi={this.email}
|
||||
disabled={this.nonAdminEditingAdmin()}
|
||||
/>
|
||||
</div>
|
||||
{!this.isEmailConfirmed() && this.userIsAdmin(app.session.user) ? (
|
||||
<div>
|
||||
{Button.component(
|
||||
{
|
||||
className: 'Button Button--block',
|
||||
loading: this.loading,
|
||||
onclick: this.activate.bind(this),
|
||||
},
|
||||
app.translator.trans('core.lib.edit_user.activate_button')
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>,
|
||||
30
|
||||
);
|
||||
|
||||
items.add(
|
||||
'password',
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('core.lib.edit_user.password_heading')}</label>
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
onchange={(e) => {
|
||||
this.setPassword(e.target.checked);
|
||||
m.redraw.sync();
|
||||
if (e.target.checked) this.$('[name=password]').select();
|
||||
e.redraw = false;
|
||||
}}
|
||||
disabled={this.nonAdminEditingAdmin()}
|
||||
/>
|
||||
{app.translator.trans('core.lib.edit_user.set_password_label')}
|
||||
</label>
|
||||
{this.setPassword() ? (
|
||||
<input
|
||||
className="FormControl"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={extractText(app.translator.trans('core.lib.edit_user.password_label'))}
|
||||
bidi={this.password}
|
||||
disabled={this.nonAdminEditingAdmin()}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
20
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (app.session.user.canEditGroups()) {
|
||||
items.add(
|
||||
'groups',
|
||||
<div className="Form-group EditUserModal-groups">
|
||||
<label>{app.translator.trans('core.lib.edit_user.groups_heading')}</label>
|
||||
<div>
|
||||
{Object.keys(this.groups)
|
||||
.map((id) => app.store.getById('groups', id))
|
||||
.map((group) => (
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
bidi={this.groups[group.id()]}
|
||||
disabled={group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user))}
|
||||
/>
|
||||
{GroupBadge.component({ group, label: '' })} {group.nameSingular()}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
items.add(
|
||||
'submit',
|
||||
<div className="Form-group">
|
||||
{Button.component(
|
||||
{
|
||||
className: 'Button Button--primary',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
},
|
||||
app.translator.trans('core.lib.edit_user.submit_button')
|
||||
)}
|
||||
</div>,
|
||||
-10
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
activate() {
|
||||
this.loading = true;
|
||||
const data = {
|
||||
username: this.username(),
|
||||
isEmailConfirmed: true,
|
||||
};
|
||||
this.attrs.user
|
||||
.save(data, { errorHandler: this.onerror.bind(this) })
|
||||
.then(() => {
|
||||
this.isEmailConfirmed(true);
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
data() {
|
||||
const data = {
|
||||
relationships: {},
|
||||
};
|
||||
|
||||
if (this.attrs.user.canEditCredentials() && !this.nonAdminEditingAdmin()) {
|
||||
data.username = this.username();
|
||||
|
||||
if (app.session.user !== this.attrs.user) {
|
||||
data.email = this.email();
|
||||
}
|
||||
|
||||
if (this.setPassword()) {
|
||||
data.password = this.password();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.attrs.user.canEditGroups()) {
|
||||
data.relationships.groups = Object.keys(this.groups)
|
||||
.filter((id) => this.groups[id]())
|
||||
.map((id) => app.store.getById('groups', id));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.attrs.user
|
||||
.save(this.data(), { errorHandler: this.onerror.bind(this) })
|
||||
.then(this.hide.bind(this))
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
nonAdminEditingAdmin() {
|
||||
return this.userIsAdmin(this.attrs.user) && !this.userIsAdmin(app.session.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @protected
|
||||
*/
|
||||
userIsAdmin(user) {
|
||||
return user.groups().some((g) => g.id() === Group.ADMINISTRATOR_ID);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user