1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-30 08:26:59 +02:00

Server: Improved Items table and added item size to it

This commit is contained in:
Laurent Cozic 2021-05-17 17:02:15 +02:00
parent a3f8cd4850
commit 7f05420fda
16 changed files with 2668 additions and 141 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "1.8.2",
"version": "2.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@joplin/renderer",
"version": "1.8.2",
"version": "2.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -6726,6 +6726,11 @@
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
},
"pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
},
"pretty-format": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",

View File

@ -32,6 +32,7 @@
"node-env-file": "^0.1.8",
"nodemon": "^2.0.6",
"pg": "^8.5.1",
"pretty-bytes": "^5.6.0",
"query-string": "^6.8.3",
"sqlite3": "^4.1.0",
"yargs": "^14.0.0"

View File

@ -43,4 +43,8 @@ table.table .nowrap {
table.table .stretch {
width: 100%;
}
table.table th .sort-button i {
margin-left: 0.5rem;
}

View File

@ -108,7 +108,7 @@ export default class ChangeModel extends BaseModel<Change> {
const query = this.changesForUserQuery(userId);
const countQuery = query.clone();
const itemCount = (await countQuery.count('id', { as: 'total' }))[0].total;
const itemCount = (await countQuery.countDistinct('id', { as: 'total' }))[0].total;
void query
.orderBy(pagination.order[0].by, pagination.order[0].dir)

View File

@ -378,8 +378,8 @@ export default class ItemModel extends BaseModel<Item> {
public async childrenCount(userId: Uuid, pathQuery: string = ''): Promise<number> {
const query = this.childrenQuery(userId, pathQuery);
query.groupBy('items.id');
return query.count();
const r = await query.countDistinct('items.id', { as: 'total' });
return r[0].total;
}
private async joplinItemPath(jopId: string): Promise<Item[]> {

View File

@ -20,8 +20,7 @@ export interface Pagination {
cursor?: string;
}
interface PaginationQueryParams {
export interface PaginationQueryParams {
limit?: number;
order_by?: string;
order_dir?: string;

View File

@ -1,4 +1,4 @@
import { beforeAllDb, afterAllTests, beforeEachDb, createItemTree, createUserAndSession } from '../../utils/testing/testUtils';
import { beforeAllDb, afterAllTests, beforeEachDb, createItemTree, createUserAndSession, parseHtml } from '../../utils/testing/testUtils';
import { execRequest } from '../../utils/testing/apiUtils';
describe('index_items', function() {
@ -26,18 +26,27 @@ describe('index_items', function() {
await createItemTree(user1.id, '', items);
// Just some basic tests to check that we're seeing at least the first
// and last item of each page.
// and last item of each page. And that the navigation bar is there with
// the right elements.
{
const response: string = await execRequest(session1.id, 'GET', 'items');
const navLinks = parseHtml(response).querySelectorAll('.pagination-link');
expect(response.includes('00000000000000000000000000000001.md')).toBe(true);
expect(response.includes('00000000000000000000000000000100.md')).toBe(true);
expect(navLinks.length).toBe(2);
expect(navLinks[0].getAttribute('class')).toContain('is-current');
expect(navLinks[1].getAttribute('class')).not.toContain('is-current');
}
{
const response: string = await execRequest(session1.id, 'GET', 'items', null, { query: { page: 2 } });
const navLinks = parseHtml(response).querySelectorAll('.pagination-link');
expect(response.includes('00000000000000000000000000000101.md')).toBe(true);
expect(response.includes('00000000000000000000000000000150.md')).toBe(true);
expect(navLinks.length).toBe(2);
expect(navLinks[0].getAttribute('class')).not.toContain('is-current');
expect(navLinks[1].getAttribute('class')).toContain('is-current');
}
});

View File

@ -3,61 +3,70 @@ import Router from '../../utils/Router';
import { AppContext } from '../../utils/types';
import { formParse } from '../../utils/requestUtils';
import { ErrorNotFound } from '../../utils/errors';
import { Item } from '../../db';
import { createPaginationLinks, filterPaginationQueryParams, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
import { setQueryParameters } from '../../utils/urlUtils';
import config from '../../config';
import { formatDateTime } from '../../utils/time';
import defaultView from '../../utils/defaultView';
import { View } from '../../services/MustacheService';
function makeFilePagination(query: any): Pagination {
const limit = Number(query.limit) || pageMaxSize;
const order: PaginationOrder[] = requestPaginationOrder(query, 'name', PaginationOrderDir.ASC);
const page: number = 'page' in query ? Number(query.page) : 1;
const output: Pagination = { limit, order, page };
validatePagination(output);
return output;
}
import { makeTablePagination, makeTableView, Row, Table, tablePartials } from '../../utils/views/table';
const prettyBytes = require('pretty-bytes');
const router = new Router();
router.get('items', async (_path: SubPath, ctx: AppContext) => {
// Query parameters that should be appended to pagination-related URLs
const baseUrlQuery = filterPaginationQueryParams(ctx.query);
const pagination = makeTablePagination(ctx.query);
const paginatedItems = await ctx.models.item().children(ctx.owner.id, '', pagination, { fields: ['id', 'name', 'updated_time', 'mime_type', 'content_size'] });
const pagination = makeFilePagination(ctx.query);
const owner = ctx.owner;
const itemModel = ctx.models.item();
const paginatedItems = await itemModel.children(owner.id, '', pagination, { fields: ['id', 'name', 'updated_time', 'mime_type'] });
const pageCount = Math.ceil((await itemModel.childrenCount(owner.id, '')) / pagination.limit);
const parentBaseUrl = itemModel.itemUrl();
const paginationLinks = createPaginationLinks(pagination.page, pageCount, setQueryParameters(parentBaseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
const table: Table = {
baseUrl: ctx.models.item().itemUrl(),
requestQuery: ctx.query,
totalItemCount: await ctx.models.item().childrenCount(ctx.owner.id, ''),
pagination,
headers: [
{
name: 'name',
label: 'Name',
stretch: true,
},
{
name: 'content_size',
label: 'Size',
},
{
name: 'mime_type',
label: 'Mime',
},
{
name: 'updated_time',
label: 'Timestamp',
},
],
rows: paginatedItems.items.map(item => {
const row: Row = [
{
value: item.name,
stretch: true,
url: `${config().baseUrl}/items/${item.id}/content`,
},
{
value: prettyBytes(item.content_size),
},
{
value: item.mime_type || 'binary',
},
{
value: formatDateTime(item.updated_time),
},
];
async function itemToViewItem(item: Item): Promise<any> {
return {
name: item.name,
url: `${config().baseUrl}/items/${item.id}/content`,
type: 'file',
icon: 'far fa-file',
timestamp: formatDateTime(item.updated_time),
mime: item.mime_type || 'binary',
};
}
const items: any[] = [];
for (const item of paginatedItems.items) {
items.push(await itemToViewItem(item));
}
return row;
}),
};
const view: View = defaultView('items');
view.content.paginatedFiles = { ...paginatedItems, items: items };
view.content.paginationLinks = paginationLinks;
view.content.itemTable = makeTableView(table),
view.content.postUrl = `${config().baseUrl}/items`;
view.cssFiles = ['index/items'];
view.partials.push('pagination');
view.partials = view.partials.concat(tablePartials());
return view;
});

View File

@ -0,0 +1,111 @@
import { createPaginationLinks, filterPaginationQueryParams, PageLink, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, PaginationQueryParams, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
import { setQueryParameters } from '../urlUtils';
const defaultSortOrder = PaginationOrderDir.ASC;
function headerIsSelectedClass(name: string, pagination: Pagination): string {
const orderBy = pagination.order[0].by;
return name === orderBy ? 'is-selected' : '';
}
function headerSortIconDir(name: string, pagination: Pagination): string {
const orderBy = pagination.order[0].by;
const orderDir = orderBy === name ? pagination.order[0].dir : defaultSortOrder;
return orderDir === PaginationOrderDir.ASC ? 'up' : 'down';
}
function headerNextOrder(name: string, pagination: Pagination): PaginationOrderDir {
if (name !== pagination.order[0].by) return defaultSortOrder;
return pagination.order[0].dir === PaginationOrderDir.ASC ? PaginationOrderDir.DESC : PaginationOrderDir.ASC;
}
export interface Header {
name: string;
label: string;
stretch?: boolean;
}
interface HeaderView {
label: string;
sortLink: string;
classNames: string[];
iconDir: string;
}
interface RowItem {
value: string;
url?: string;
stretch?: boolean;
}
export type Row = RowItem[];
interface RowItemView {
value: string;
classNames: string[];
url: string;
}
type RowView = RowItemView[];
export interface Table {
headers: Header[];
rows: Row[];
baseUrl: string;
requestQuery: any;
totalItemCount: number;
pagination: Pagination;
}
export interface TableView {
headers: HeaderView[];
rows: RowView[];
paginationLinks: PageLink[];
}
export function makeTablePagination(query: any): Pagination {
const limit = Number(query.limit) || pageMaxSize;
const order: PaginationOrder[] = requestPaginationOrder(query, 'name', PaginationOrderDir.ASC);
const page: number = 'page' in query ? Number(query.page) : 1;
const output: Pagination = { limit, order, page };
validatePagination(output);
return output;
}
function makeHeaderView(header: Header, parentBaseUrl: string, baseUrlQuery: PaginationQueryParams, pagination: Pagination): HeaderView {
return {
label: header.label,
sortLink: setQueryParameters(parentBaseUrl, { ...baseUrlQuery, 'order_by': header.name, 'order_dir': headerNextOrder(header.name, pagination) }),
classNames: [header.stretch ? 'stretch' : 'nowrap', headerIsSelectedClass(header.name, pagination)],
iconDir: headerSortIconDir(header.name, pagination),
};
}
function makeRowView(row: Row): RowView {
return row.map(rowItem => {
return {
value: rowItem.value,
classNames: [rowItem.stretch ? 'stretch' : 'nowrap'],
url: rowItem.url,
};
});
}
export function makeTableView(table: Table): TableView {
const baseUrlQuery = filterPaginationQueryParams(table.requestQuery);
const pagination = table.pagination;
const pageCount = Math.ceil(table.totalItemCount / pagination.limit);
const paginationLinks = createPaginationLinks(pagination.page, pageCount, setQueryParameters(table.baseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
return {
headers: table.headers.map(h => makeHeaderView(h, table.baseUrl, baseUrlQuery, pagination)),
rows: table.rows.map(r => makeRowView(r)),
paginationLinks,
};
}
export function tablePartials(): string[] {
return ['pagination', 'table', 'tableHeader', 'tableRowItem'];
}

View File

@ -2,28 +2,9 @@
<input type="submit" name="delete_all_button" class="button is-danger" value="Delete all" />
</form>
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th class="stretch">Name</th>
<th class="nowrap">Mime</th>
<th class="nowrap">Timestamp</th>
</tr>
</thead>
<tbody>
{{#paginatedFiles.items}}
<tr>
<td class="stretch item-{{type}}">
<a href="{{url}}"><span class="icon"><i class="{{icon}}"></i></span>{{name}}</a>
</td>
<td class="nowrap">{{mime}}</td>
<td class="nowrap">{{timestamp}}</td>
</tr>
{{/paginatedFiles.items}}
</tbody>
</table>
{{>pagination}}
{{#itemTable}}
{{>table}}
{{/itemTable}}
<script>
onDocumentReady(function() {

View File

@ -0,0 +1,20 @@
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
{{#headers}}
{{>tableHeader}}
{{/headers}}
</tr>
</thead>
<tbody>
{{#rows}}
<tr>
{{#.}}
{{>tableRowItem}}
{{/.}}
</tr>
{{/rows}}
</tbody>
</table>
{{>pagination}}

View File

@ -0,0 +1,3 @@
<th class="{{#classNames}}{{.}} {{/classNames}}">
<a href="{{sortLink}}" class="sort-button">{{label}} <i class="fas fa-caret-{{iconDir}}"></i></a>
</th>

View File

@ -0,0 +1,3 @@
<td class="{{#classNames}}{{.}} {{/classNames}}">
{{#url}}<a href="{{.}}"></span>{{/url}}{{value}}</a>
</td>

File diff suppressed because it is too large Load Diff