mirror of
https://github.com/laurent22/joplin.git
synced 2025-03-20 20:55:18 +02:00
Server: Paginate users
This commit is contained in:
parent
8ea6d89d49
commit
8ac8d537c8
@ -10,6 +10,7 @@ import personalizedUserContentBaseUrl from '@joplin/lib/services/joplinServer/pe
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import dbuuid from '../utils/dbuuid';
|
||||
import { defaultPagination, PaginatedResults, Pagination } from './utils/pagination';
|
||||
import { Knex } from 'knex';
|
||||
import { unique } from '../utils/array';
|
||||
|
||||
const logger = Logger.create('BaseModel');
|
||||
@ -33,6 +34,10 @@ export interface LoadOptions {
|
||||
fields?: string[];
|
||||
}
|
||||
|
||||
export interface AllPaginatedOptions extends LoadOptions {
|
||||
queryCallback?: (query: Knex.QueryBuilder)=> Knex.QueryBuilder;
|
||||
}
|
||||
|
||||
export interface DeleteOptions {
|
||||
validationRules?: any;
|
||||
allowNoOp?: boolean;
|
||||
@ -242,7 +247,7 @@ export default abstract class BaseModel<T> {
|
||||
return rows as T[];
|
||||
}
|
||||
|
||||
public async allPaginated(pagination: Pagination, options: LoadOptions = {}): Promise<PaginatedResults<T>> {
|
||||
public async allPaginated(pagination: Pagination, options: AllPaginatedOptions = {}): Promise<PaginatedResults<T>> {
|
||||
pagination = {
|
||||
...defaultPagination(),
|
||||
...pagination,
|
||||
@ -250,12 +255,18 @@ export default abstract class BaseModel<T> {
|
||||
|
||||
const itemCount = await this.count();
|
||||
|
||||
const items = await this
|
||||
let query = this
|
||||
.db(this.tableName)
|
||||
.select(this.selectFields(options))
|
||||
.select(this.selectFields(options));
|
||||
|
||||
if (options.queryCallback) query = options.queryCallback(query);
|
||||
|
||||
void query
|
||||
.orderBy(pagination.order[0].by, pagination.order[0].dir)
|
||||
.offset((pagination.page - 1) * pagination.limit)
|
||||
.limit(pagination.limit) as T[];
|
||||
.limit(pagination.limit);
|
||||
|
||||
const items = (await query) as T[];
|
||||
|
||||
return {
|
||||
items,
|
||||
|
@ -168,7 +168,7 @@ export function createPaginationLinks(page: number, pageCount: number, urlTempla
|
||||
firstPages.push({ page: p });
|
||||
}
|
||||
|
||||
if (firstPages.length && (output[0].page - firstPages[firstPages.length - 1].page) > 1) {
|
||||
if (firstPages.length && output.length && (output[0].page - firstPages[firstPages.length - 1].page) > 1) {
|
||||
firstPages.push({ isEllipsis: true });
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SubPath, redirect } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { Knex } from 'knex';
|
||||
import { AppContext, HttpMethod } from '../../utils/types';
|
||||
import { contextSessionId, formParse } from '../../utils/requestUtils';
|
||||
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
|
||||
@ -14,13 +15,15 @@ import uuidgen from '../../utils/uuidgen';
|
||||
import { formatMaxItemSize, formatMaxTotalSize, formatTotalSize, formatTotalSizePercent, yesOrNo } from '../../utils/strings';
|
||||
import { getCanShareFolder, totalSizeClass } from '../../models/utils/user';
|
||||
import { yesNoDefaultOptions, yesNoOptions } from '../../utils/views/select';
|
||||
import { stripePortalUrl, adminUserDeletionsUrl, adminUserUrl } from '../../utils/urlUtils';
|
||||
import { stripePortalUrl, adminUserDeletionsUrl, adminUserUrl, adminUsersUrl, setQueryParameters } from '../../utils/urlUtils';
|
||||
import { cancelSubscriptionByUserId, updateSubscriptionType } from '../../utils/stripe';
|
||||
import { createCsrfTag } from '../../utils/csrf';
|
||||
import { formatDateTime, Hour } from '../../utils/time';
|
||||
import { startImpersonating, stopImpersonating } from './utils/users/impersonate';
|
||||
import { userFlagToString } from '../../models/UserFlagModel';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { makeTablePagination, makeTableView, Row, Table } from '../../utils/views/table';
|
||||
import { PaginationOrderDir } from '../../models/utils/pagination';
|
||||
|
||||
export interface CheckRepeatPasswordInput {
|
||||
password: string;
|
||||
@ -95,33 +98,96 @@ router.get('admin/users', async (_path: SubPath, ctx: AppContext) => {
|
||||
const userModel = ctx.joplin.models.user();
|
||||
await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.List);
|
||||
|
||||
const users = await userModel.all();
|
||||
const showDisabled = ctx.query.show_disabled === '1';
|
||||
|
||||
users.sort((u1: User, u2: User) => {
|
||||
if (u1.full_name && u2.full_name) return u1.full_name.toLowerCase() < u2.full_name.toLowerCase() ? -1 : +1;
|
||||
if (u1.full_name && !u2.full_name) return +1;
|
||||
if (!u1.full_name && u2.full_name) return -1;
|
||||
return u1.email.toLowerCase() < u2.email.toLowerCase() ? -1 : +1;
|
||||
const pagination = makeTablePagination(ctx.query, 'full_name', PaginationOrderDir.ASC);
|
||||
pagination.limit = 1000;
|
||||
const page = await ctx.joplin.models.user().allPaginated(pagination, {
|
||||
queryCallback: (query: Knex.QueryBuilder) => {
|
||||
if (!showDisabled) {
|
||||
void query.where('enabled', '=', 1);
|
||||
}
|
||||
return query;
|
||||
},
|
||||
});
|
||||
|
||||
const view: View = defaultView('admin/users', _('Users'));
|
||||
view.content = {
|
||||
users: users.map(user => {
|
||||
return {
|
||||
...user,
|
||||
url: adminUserUrl(user.id),
|
||||
displayName: user.full_name ? user.full_name : '(not set)',
|
||||
formattedItemMaxSize: formatMaxItemSize(user),
|
||||
formattedTotalSize: formatTotalSize(user),
|
||||
formattedMaxTotalSize: formatMaxTotalSize(user),
|
||||
formattedTotalSizePercent: formatTotalSizePercent(user),
|
||||
totalSizeClass: totalSizeClass(user),
|
||||
formattedAccountType: accountTypeToString(user.account_type),
|
||||
formattedCanShareFolder: yesOrNo(getCanShareFolder(user)),
|
||||
rowClassName: user.enabled ? '' : 'is-disabled',
|
||||
const table: Table = {
|
||||
baseUrl: adminUsersUrl(),
|
||||
requestQuery: ctx.query,
|
||||
pageCount: page.page_count,
|
||||
pagination,
|
||||
headers: [
|
||||
{
|
||||
name: 'full_name',
|
||||
label: _('Full name'),
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: _('Email'),
|
||||
},
|
||||
{
|
||||
name: 'account',
|
||||
label: _('Account'),
|
||||
},
|
||||
{
|
||||
name: 'max_item_size',
|
||||
label: _('Max Item Size'),
|
||||
},
|
||||
{
|
||||
name: 'total_size',
|
||||
label: _('Total Size'),
|
||||
},
|
||||
{
|
||||
name: 'max_total_size',
|
||||
label: _('Max Total Size'),
|
||||
},
|
||||
{
|
||||
name: 'can_share',
|
||||
label: _('Can Share'),
|
||||
},
|
||||
],
|
||||
rows: page.items.map(user => {
|
||||
const row: Row = {
|
||||
classNames: [user.enabled ? '' : 'is-disabled'],
|
||||
items: [
|
||||
{
|
||||
value: user.full_name ? user.full_name : '(not set)',
|
||||
url: adminUserUrl(user.id),
|
||||
},
|
||||
{
|
||||
value: user.email,
|
||||
},
|
||||
{
|
||||
value: accountTypeToString(user.account_type),
|
||||
},
|
||||
{
|
||||
value: formatMaxItemSize(user),
|
||||
},
|
||||
{
|
||||
value: `${formatTotalSize(user)} (${formatTotalSizePercent(user)})`,
|
||||
classNames: [totalSizeClass(user)],
|
||||
},
|
||||
{
|
||||
value: formatMaxTotalSize(user),
|
||||
},
|
||||
{
|
||||
value: yesOrNo(getCanShareFolder(user)),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return row;
|
||||
}),
|
||||
};
|
||||
|
||||
const view = defaultView('admin/users', _('Users'));
|
||||
view.content = {
|
||||
userTable: makeTableView(table),
|
||||
csrfTag: await createCsrfTag(ctx),
|
||||
disabledToggleButtonLabel: showDisabled ? _('Hide disabled') : _('Show disabled'),
|
||||
disabledToggleButtonUrl: setQueryParameters(adminUsersUrl(), { ...ctx.query, show_disabled: showDisabled ? '0' : '1' }),
|
||||
};
|
||||
|
||||
return view;
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createPaginationLinks, filterPaginationQueryParams, PageLink, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, PaginationQueryParams, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
|
||||
import { createPaginationLinks, PageLink, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, PaginationQueryParams, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
|
||||
import { setQueryParameters } from '../urlUtils';
|
||||
|
||||
const defaultSortOrder = PaginationOrderDir.ASC;
|
||||
@ -45,6 +45,7 @@ interface RowItem {
|
||||
stretch?: boolean;
|
||||
hint?: string;
|
||||
render?: RowItemRenderCallback;
|
||||
classNames?: string[];
|
||||
}
|
||||
|
||||
export interface Row {
|
||||
@ -106,10 +107,13 @@ function makeRowView(row: Row): RowView {
|
||||
return {
|
||||
classNames: row.classNames,
|
||||
items: row.items.map(rowItem => {
|
||||
let classNames = [rowItem.stretch ? 'stretch' : 'nowrap'];
|
||||
if (rowItem.classNames) classNames = classNames.concat(rowItem.classNames);
|
||||
|
||||
return {
|
||||
value: rowItem.value,
|
||||
valueHtml: rowItem.render ? rowItem.render() : '',
|
||||
classNames: [rowItem.stretch ? 'stretch' : 'nowrap'],
|
||||
classNames,
|
||||
url: rowItem.url,
|
||||
checkbox: rowItem.checkbox,
|
||||
hint: rowItem.hint,
|
||||
@ -126,7 +130,7 @@ export function makeTableView(table: Table): TableView {
|
||||
if (table.pageCount) {
|
||||
if (!table.baseUrl || !table.requestQuery) throw new Error('Table.baseUrl and Table.requestQuery are required for pagination when there is more than one page');
|
||||
|
||||
baseUrlQuery = filterPaginationQueryParams(table.requestQuery);
|
||||
baseUrlQuery = table.requestQuery; // filterPaginationQueryParams(table.requestQuery);
|
||||
pagination = table.pagination;
|
||||
paginationLinks = createPaginationLinks(pagination.page, table.pageCount, setQueryParameters(table.baseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
|
||||
}
|
||||
|
@ -1,60 +1,13 @@
|
||||
<div class="block">
|
||||
<a class="button is-primary" href="{{{global.baseUrl}}}/admin/users/new">Add user</a>
|
||||
<a class="button is-link toggle-disabled-button hide-disabled" href="#">Hide disabled</a>
|
||||
<a class="button is-link toggle-disabled-button hide-disabled" href="{{disabledToggleButtonUrl}}">{{disabledToggleButtonLabel}}</a>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Full name</th>
|
||||
<th>Email</th>
|
||||
<th>Account</th>
|
||||
<th>Max Item Size</th>
|
||||
<th>Total Size</th>
|
||||
<th>Max Total Size</th>
|
||||
<th>Can share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#users}}
|
||||
<tr class="{{rowClassName}}">
|
||||
<td><a href="{{{url}}}">{{displayName}}</a></td>
|
||||
<td>{{email}}</td>
|
||||
<td>{{formattedAccountType}}</td>
|
||||
<td>{{formattedItemMaxSize}}</td>
|
||||
<td class="{{totalSizeClass}}">{{formattedTotalSize}} ({{formattedTotalSizePercent}})</td>
|
||||
<td>{{formattedMaxTotalSize}}</td>
|
||||
<td>{{formattedCanShareFolder}}</td>
|
||||
</tr>
|
||||
{{/users}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{#userTable}}
|
||||
{{>table}}
|
||||
{{/userTable}}
|
||||
|
||||
<div class="block">
|
||||
<a class="button is-primary" href="{{{global.baseUrl}}}/admin/users/new">Add user</a>
|
||||
<a class="button is-link toggle-disabled-button hide-disabled" href="#">Hide disabled</a>
|
||||
<a class="button is-link toggle-disabled-button hide-disabled" href="{{disabledToggleButtonUrl}}">{{disabledToggleButtonLabel}}</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(() => {
|
||||
function toggleDisabled() {
|
||||
if ($('.hide-disabled').length) {
|
||||
$('.hide-disabled').addClass('show-disabled');
|
||||
$('.hide-disabled').removeClass('hide-disabled');
|
||||
$('.show-disabled').text('Show disabled');
|
||||
$('table tr.is-disabled').hide();
|
||||
} else {
|
||||
$('.show-disabled').addClass('hide-disabled');
|
||||
$('.show-disabled').removeClass('show-disabled');
|
||||
$('.hide-disabled').text('Hide disabled');
|
||||
$('table tr.is-disabled').show();
|
||||
}
|
||||
}
|
||||
|
||||
toggleDisabled();
|
||||
|
||||
$('.toggle-disabled-button').click(() => {
|
||||
toggleDisabled();
|
||||
});
|
||||
});
|
||||
</script>
|
Loading…
x
Reference in New Issue
Block a user