mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-26 18:58:21 +02:00
Server: Added log page to view latest changes to files
This commit is contained in:
parent
abe0013914
commit
874f3010b7
@ -211,6 +211,13 @@ export enum ChangeType {
|
||||
Delete = 3,
|
||||
}
|
||||
|
||||
export function changeTypeToString(t: ChangeType): string {
|
||||
if (t === ChangeType.Create) return 'create';
|
||||
if (t === ChangeType.Update) return 'update';
|
||||
if (t === ChangeType.Delete) return 'delete';
|
||||
throw new Error(`Unkown type: ${t}`);
|
||||
}
|
||||
|
||||
export enum ShareType {
|
||||
Link = 1, // When a note is shared via a public link
|
||||
App = 2, // When a note is shared with another user on the same server instance
|
||||
|
@ -44,7 +44,7 @@ export default async function(ctx: AppContext) {
|
||||
});
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = responseObject;
|
||||
ctx.response.body = [undefined, null].includes(responseObject) ? '' : responseObject;
|
||||
}
|
||||
} else {
|
||||
throw new ErrorNotFound();
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Change, ChangeType, File, ItemType, Uuid } from '../db';
|
||||
import { ErrorResyncRequired, ErrorUnprocessableEntity } from '../utils/errors';
|
||||
import BaseModel from './BaseModel';
|
||||
import { PaginatedResults } from './utils/pagination';
|
||||
import { paginateDbQuery, PaginatedResults, Pagination } from './utils/pagination';
|
||||
|
||||
export interface ChangeWithItem {
|
||||
item: File;
|
||||
updated_time: number;
|
||||
type: ChangeType;
|
||||
}
|
||||
|
||||
@ -47,6 +48,25 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
return this.save(change) as Change;
|
||||
}
|
||||
|
||||
private async countByUser(userId: string): Promise<number> {
|
||||
const r: any = await this.db(this.tableName).where('owner_id', userId).count('id', { as: 'total' }).first();
|
||||
return r.total;
|
||||
}
|
||||
|
||||
public changeUrl(): string {
|
||||
return `${this.baseUrl}/changes`;
|
||||
}
|
||||
|
||||
public async allWithPagination(pagination: Pagination): Promise<PaginatedChanges> {
|
||||
const results = await paginateDbQuery(this.db(this.tableName).select(...this.defaultFields).where('owner_id', '=', this.userId), pagination);
|
||||
const changeWithItems = await this.loadChangeItems(results.items);
|
||||
return {
|
||||
...results,
|
||||
items: changeWithItems,
|
||||
page_count: Math.ceil(await this.countByUser(this.userId) / pagination.limit),
|
||||
};
|
||||
}
|
||||
|
||||
// Note: doesn't currently support checking for changes recursively but this
|
||||
// is not needed for Joplin synchronisation.
|
||||
public async byDirectoryId(dirId: string, pagination: ChangePagination = null): Promise<PaginatedChanges> {
|
||||
@ -129,6 +149,7 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
|
||||
output.push({
|
||||
type: change.type,
|
||||
updated_time: change.updated_time,
|
||||
item: item,
|
||||
});
|
||||
}
|
||||
|
@ -98,6 +98,11 @@ export default class FileModel extends BaseModel<File> {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async itemDisplayPath(item: File, loadOptions: LoadOptions = {}): Promise<string> {
|
||||
const path = await this.itemFullPath(item, loadOptions);
|
||||
return this.removeTrailingColons(path.replace(/root:\//, ''));
|
||||
}
|
||||
|
||||
public async itemFullPath(item: File, loadOptions: LoadOptions = {}): Promise<string> {
|
||||
const segments: string[] = [];
|
||||
while (item) {
|
||||
@ -347,7 +352,8 @@ export default class FileModel extends BaseModel<File> {
|
||||
|
||||
public async fileUrl(idOrPath: string, query: any = null): Promise<string> {
|
||||
const file: File = await this.pathToFile(idOrPath);
|
||||
return setQueryParameters(`${this.baseUrl}/files/${await this.itemFullPath(file)}`, query);
|
||||
const contentSuffix = !file.is_directory ? '/content' : '';
|
||||
return setQueryParameters(`${this.baseUrl}/files/${await this.itemFullPath(file)}${contentSuffix}`, query);
|
||||
}
|
||||
|
||||
private async pathToFiles(path: string, mustExist: boolean = true): Promise<File[]> {
|
||||
|
@ -33,6 +33,7 @@ export interface PaginatedResults {
|
||||
items: any[];
|
||||
has_more: boolean;
|
||||
cursor?: string;
|
||||
page_count?: number;
|
||||
}
|
||||
|
||||
export const pageMaxSize = 100;
|
||||
@ -135,6 +136,15 @@ export function paginationToQueryParams(pagination: Pagination): PaginationQuery
|
||||
return output;
|
||||
}
|
||||
|
||||
export function queryParamsToPagination(query: PaginationQueryParams): Pagination {
|
||||
const limit = Number(query.limit) || pageMaxSize;
|
||||
const order: PaginationOrder[] = requestPaginationOrder(query);
|
||||
const page: number = 'page' in query ? Number(query.page) : 1;
|
||||
const output: Pagination = { limit, order, page };
|
||||
validatePagination(output);
|
||||
return output;
|
||||
}
|
||||
|
||||
export interface PageLink {
|
||||
page?: number;
|
||||
isEllipsis?: boolean;
|
||||
@ -142,6 +152,14 @@ export interface PageLink {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export function filterPaginationQueryParams(query: any): PaginationQueryParams {
|
||||
const baseUrlQuery: PaginationQueryParams = {};
|
||||
if (query.limit) baseUrlQuery.limit = query.limit;
|
||||
if (query.order_by) baseUrlQuery.order_by = query.order_by;
|
||||
if (query.order_dir) baseUrlQuery.order_dir = query.order_dir;
|
||||
return baseUrlQuery;
|
||||
}
|
||||
|
||||
export function createPaginationLinks(page: number, pageCount: number, urlTemplate: string = null): PageLink[] {
|
||||
if (!pageCount) return [];
|
||||
|
||||
|
86
packages/server/src/routes/index/changes.ts
Normal file
86
packages/server/src/routes/index/changes.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { SubPath } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { changeTypeToString, ChangeType } from '../../db';
|
||||
import { createPaginationLinks, filterPaginationQueryParams, queryParamsToPagination } from '../../models/utils/pagination';
|
||||
import { setQueryParameters } from '../../utils/urlUtils';
|
||||
import { formatDateTime } from '../../utils/time';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
|
||||
interface ItemToDisplay {
|
||||
name: string;
|
||||
type: string;
|
||||
changeType: string;
|
||||
timestamp: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get('changes', async (_path: SubPath, ctx: AppContext) => {
|
||||
const changeModel = ctx.models.change({ userId: ctx.owner.id });
|
||||
const fileModel = ctx.models.file({ userId: ctx.owner.id });
|
||||
|
||||
const pagination = queryParamsToPagination(ctx.query);
|
||||
|
||||
// {
|
||||
// "items": [
|
||||
// {
|
||||
// "type": 3,
|
||||
// "item": {
|
||||
// "id": "QZbQVWTCtr9qpxtEsuWMoQbax8wR1Q75",
|
||||
// "name": "sync_desktop_bbecbb2d6bf44a16aa14c14f6c51719d.json"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "type": 1,
|
||||
// "item": {
|
||||
// "id": "8ogKqMu58u1FcZ9gaBO1yqPHKzniZSfx",
|
||||
// "owner_id": "Pg8NSIS3fo7sotSktqb2Rza7EJFcpj3M",
|
||||
// "name": "ab9e895491844213a43338608deaf573.md",
|
||||
// "mime_type": "text/markdown",
|
||||
// "size": 908,
|
||||
// "is_directory": 0,
|
||||
// "is_root": 0,
|
||||
// "parent_id": "5IhOFX314EZOL21p9UUVKZElgjhuUerV",
|
||||
// "updated_time": 1616235197809,
|
||||
// "created_time": 1616235197809
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
const paginatedChanges = await changeModel.allWithPagination(pagination);
|
||||
const itemsToDisplay: ItemToDisplay[] = [];
|
||||
|
||||
for (const item of paginatedChanges.items) {
|
||||
itemsToDisplay.push({
|
||||
name: await fileModel.itemDisplayPath(item.item),
|
||||
type: item.item.is_directory ? 'd' : 'f',
|
||||
changeType: changeTypeToString(item.type),
|
||||
timestamp: formatDateTime(item.updated_time),
|
||||
url: item.type !== ChangeType.Delete ? await fileModel.fileUrl(item.item.id) : '',
|
||||
});
|
||||
}
|
||||
|
||||
const paginationLinks = createPaginationLinks(
|
||||
pagination.page,
|
||||
paginatedChanges.page_count,
|
||||
setQueryParameters(
|
||||
changeModel.changeUrl(), {
|
||||
...filterPaginationQueryParams(ctx.query),
|
||||
'page': 'PAGE_NUMBER',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const view: View = defaultView('changes');
|
||||
view.content.paginatedChanges = { ...paginatedChanges, items: itemsToDisplay };
|
||||
view.content.paginationLinks = paginationLinks;
|
||||
view.cssFiles = ['index/changes'];
|
||||
view.partials.push('pagination');
|
||||
return view;
|
||||
});
|
||||
|
||||
export default router;
|
@ -4,7 +4,7 @@ import { AppContext, HttpMethod } from '../../utils/types';
|
||||
import { contextSessionId, formParse } from '../../utils/requestUtils';
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
import { File } from '../../db';
|
||||
import { createPaginationLinks, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
|
||||
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';
|
||||
@ -28,15 +28,11 @@ router.alias(HttpMethod.GET, 'files', 'files/:id');
|
||||
|
||||
router.get('files/:id', async (path: SubPath, ctx: AppContext) => {
|
||||
const dirId = path.id;
|
||||
const query = ctx.query;
|
||||
|
||||
// Query parameters that should be appended to pagination-related URLs
|
||||
const baseUrlQuery: any = {};
|
||||
if (query.limit) baseUrlQuery.limit = query.limit;
|
||||
if (query.order_by) baseUrlQuery.order_by = query.order_by;
|
||||
if (query.order_dir) baseUrlQuery.order_dir = query.order_dir;
|
||||
const baseUrlQuery = filterPaginationQueryParams(ctx.query);
|
||||
|
||||
const pagination = makeFilePagination(query);
|
||||
const pagination = makeFilePagination(ctx.query);
|
||||
const owner = ctx.owner;
|
||||
const fileModel = ctx.models.file({ userId: owner.id });
|
||||
const root = await fileModel.userRootFile();
|
||||
|
@ -12,6 +12,7 @@ import indexUsers from './index/users';
|
||||
import indexFiles from './index/files';
|
||||
import indexNotifications from './index/notifications';
|
||||
import indexShares from './index/shares';
|
||||
import indexChanges from './index/changes';
|
||||
|
||||
import defaultRoute from './default';
|
||||
|
||||
@ -28,6 +29,7 @@ const routes: Routers = {
|
||||
'files': indexFiles,
|
||||
'notifications': indexNotifications,
|
||||
'shares': indexShares,
|
||||
'changes': indexChanges,
|
||||
|
||||
'': defaultRoute,
|
||||
};
|
||||
|
29
packages/server/src/views/index/changes.mustache
Normal file
29
packages/server/src/views/index/changes.mustache
Normal file
@ -0,0 +1,29 @@
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="stretch">Item</th>
|
||||
<th class="nowrap">Type</th>
|
||||
<th class="nowrap">Change</th>
|
||||
<th class="nowrap">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#paginatedChanges.items}}
|
||||
<tr>
|
||||
<td class="stretch item-{{type}}">
|
||||
{{#url}}
|
||||
<a href="{{url}}">{{name}}</a>
|
||||
{{/url}}
|
||||
{{^url}}
|
||||
{{name}}
|
||||
{{/url}}
|
||||
</td>
|
||||
<td class="nowrap">{{type}}</td>
|
||||
<td class="nowrap">{{changeType}}</td>
|
||||
<td class="nowrap">{{timestamp}}</td>
|
||||
</tr>
|
||||
{{/paginatedChanges.items}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{>pagination}}
|
@ -11,6 +11,7 @@
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/users">Users</a>
|
||||
{{/global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/files">Files</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">Log</a>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">{{global.owner.email}}</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user