client/snapshots: add snapshots browser
This commit is contained in:
parent
80af79779d
commit
9014baab92
12 changed files with 338 additions and 27 deletions
44
client/css/snapshots.styl
Normal file
44
client/css/snapshots.styl
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
$snapshot-created-background-color = #E0F5E0
|
||||||
|
$snapshot-modified-background-color = #E0F5FF
|
||||||
|
$snapshot-deleted-background-color = #FDE5E5
|
||||||
|
$snapshot-merged-background-color = #FEC
|
||||||
|
|
||||||
|
.snapshot-list
|
||||||
|
text-align: left
|
||||||
|
|
||||||
|
ul
|
||||||
|
margin: 0 auto
|
||||||
|
width: 100%
|
||||||
|
max-width: 35em
|
||||||
|
list-style-type: none
|
||||||
|
|
||||||
|
li
|
||||||
|
.time
|
||||||
|
float: right
|
||||||
|
|
||||||
|
div
|
||||||
|
padding: 0.1em 0.5em
|
||||||
|
.thumbnail
|
||||||
|
margin-left: 0
|
||||||
|
&:empty
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
div.operation-created
|
||||||
|
background: $snapshot-created-background-color
|
||||||
|
&+.details
|
||||||
|
background: lighten($snapshot-created-background-color, 50%)
|
||||||
|
div.operation-modified
|
||||||
|
background: $snapshot-modified-background-color
|
||||||
|
&+.details
|
||||||
|
background: lighten($snapshot-modified-background-color, 50%)
|
||||||
|
div.operation-deleted
|
||||||
|
background: $snapshot-deleted-background-color
|
||||||
|
&+.details
|
||||||
|
background: lighten($snapshot-deleted-background-color, 50%)
|
||||||
|
div.operation-merged
|
||||||
|
background: $snapshot-merged-background-color
|
||||||
|
&+.details
|
||||||
|
background: lighten($snapshot-merged-background-color, 50%)
|
||||||
|
|
||||||
|
div.details
|
||||||
|
margin-bottom: 2em
|
|
@ -1,7 +1,7 @@
|
||||||
<div class='post-container'></div>
|
<div class='post-container'></div>
|
||||||
<% if (ctx.featuredPost) { %>
|
<% if (ctx.featuredPost) { %>
|
||||||
<aside>
|
<aside>
|
||||||
Featured post: <%= ctx.makePostLink(ctx.featuredPost.id) %>,
|
Featured post: <%= ctx.makePostLink(ctx.featuredPost.id, true) %>,
|
||||||
posted
|
posted
|
||||||
<%= ctx.makeRelativeTime(ctx.featuredPost.creationTime) %>
|
<%= ctx.makeRelativeTime(ctx.featuredPost.creationTime) %>
|
||||||
by
|
by
|
||||||
|
|
|
@ -4,3 +4,7 @@
|
||||||
<span class=sep>•</span>
|
<span class=sep>•</span>
|
||||||
Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a>
|
Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a>
|
||||||
from <%= ctx.makeRelativeTime(ctx.buildDate) %>
|
from <%= ctx.makeRelativeTime(ctx.buildDate) %>
|
||||||
|
<% if (ctx.canListSnapshots) { %>
|
||||||
|
<span class=sep>•</span>
|
||||||
|
<a href='/history'>History</a>
|
||||||
|
<% } %>
|
||||||
|
|
31
client/html/snapshots_page.tpl
Normal file
31
client/html/snapshots_page.tpl
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<div class='snapshot-list'>
|
||||||
|
<% if (ctx.results.length) { %>
|
||||||
|
<ul>
|
||||||
|
<% for (let item of ctx.results) { %>
|
||||||
|
<li>
|
||||||
|
<div class='header operation-<%= item.operation %>'>
|
||||||
|
<span class='time'>
|
||||||
|
<%= ctx.makeRelativeTime(item.time) %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<%= ctx.makeUserLink(item.user) %>
|
||||||
|
|
||||||
|
<%= item.operation %>
|
||||||
|
|
||||||
|
<%= ctx.makeResourceLink(item.type, item.id) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='details'><!--
|
||||||
|
--><% if (item.operation === 'created') { %><!--
|
||||||
|
--><%= ctx.makeItemCreation(item.type, item.data) %><!--
|
||||||
|
--><% } else if (item.operation === 'modified') { %><!--
|
||||||
|
--><%= ctx.makeItemModification(item.type, item.data) %><!--
|
||||||
|
--><% } else if (item.operation === 'merged') { %><!--
|
||||||
|
-->Merged to <%= ctx.makeResourceLink(item.data[0], item.data[1]) %><!--
|
||||||
|
--><% } %><!--
|
||||||
|
--></div>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
|
</ul>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
|
@ -1,16 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
|
||||||
|
|
||||||
class HistoryController {
|
|
||||||
constructor() {
|
|
||||||
topNavigation.activate('');
|
|
||||||
topNavigation.setTitle('History');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = router => {
|
|
||||||
router.enter('/history', (ctx, next) => {
|
|
||||||
ctx.controller = new HistoryController();
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -15,6 +15,7 @@ class HomeController {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
version: config.meta.version,
|
version: config.meta.version,
|
||||||
buildDate: config.meta.buildDate,
|
buildDate: config.meta.buildDate,
|
||||||
|
canListSnapshots: api.hasPrivilege('snapshots:list'),
|
||||||
canListPosts: api.hasPrivilege('posts:list'),
|
canListPosts: api.hasPrivilege('posts:list'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
41
client/js/controllers/snapshots_controller.js
Normal file
41
client/js/controllers/snapshots_controller.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const api = require('../api.js');
|
||||||
|
const misc = require('../util/misc.js');
|
||||||
|
const SnapshotList = require('../models/snapshot_list.js');
|
||||||
|
const PageController = require('../controllers/page_controller.js');
|
||||||
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
|
const SnapshotsPageView = require('../views/snapshots_page_view.js');
|
||||||
|
|
||||||
|
class SnapshotsController {
|
||||||
|
constructor(ctx) {
|
||||||
|
topNavigation.activate('');
|
||||||
|
topNavigation.setTitle('History');
|
||||||
|
|
||||||
|
this._pageController = new PageController({
|
||||||
|
parameters: ctx.parameters,
|
||||||
|
getClientUrlForPage: page => {
|
||||||
|
const parameters = Object.assign(
|
||||||
|
{}, ctx.parameters, {page: page});
|
||||||
|
return '/history/' + misc.formatUrlParameters(parameters);
|
||||||
|
},
|
||||||
|
requestPage: page => {
|
||||||
|
return SnapshotList.search('', page, 25);
|
||||||
|
},
|
||||||
|
pageRenderer: pageCtx => {
|
||||||
|
Object.assign(pageCtx, {
|
||||||
|
canViewPosts: api.hasPrivilege('posts:view'),
|
||||||
|
canViewUsers: api.hasPrivilege('users:view'),
|
||||||
|
canViewTags: api.hasPrivilege('tags:view'),
|
||||||
|
});
|
||||||
|
return new SnapshotsPageView(pageCtx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = router => {
|
||||||
|
router.enter('/history/:parameters?',
|
||||||
|
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
|
||||||
|
(ctx, next) => { ctx.controller = new SnapshotsController(ctx); });
|
||||||
|
};
|
|
@ -33,7 +33,7 @@ controllers.push(require('./controllers/help_controller.js'));
|
||||||
controllers.push(require('./controllers/auth_controller.js'));
|
controllers.push(require('./controllers/auth_controller.js'));
|
||||||
controllers.push(require('./controllers/password_reset_controller.js'));
|
controllers.push(require('./controllers/password_reset_controller.js'));
|
||||||
controllers.push(require('./controllers/comments_controller.js'));
|
controllers.push(require('./controllers/comments_controller.js'));
|
||||||
controllers.push(require('./controllers/history_controller.js'));
|
controllers.push(require('./controllers/snapshots_controller.js'));
|
||||||
controllers.push(require('./controllers/post_controller.js'));
|
controllers.push(require('./controllers/post_controller.js'));
|
||||||
controllers.push(require('./controllers/post_list_controller.js'));
|
controllers.push(require('./controllers/post_list_controller.js'));
|
||||||
controllers.push(require('./controllers/post_upload_controller.js'));
|
controllers.push(require('./controllers/post_upload_controller.js'));
|
||||||
|
|
40
client/js/models/snapshot.js
Normal file
40
client/js/models/snapshot.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const api = require('../api.js');
|
||||||
|
const events = require('../events.js');
|
||||||
|
|
||||||
|
class Snapshot extends events.EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._orig = {};
|
||||||
|
this._updateFromResponse({});
|
||||||
|
}
|
||||||
|
|
||||||
|
get operation() { return this._operation; }
|
||||||
|
get type() { return this._type; }
|
||||||
|
get id() { return this._id; }
|
||||||
|
get user() { return this._user; }
|
||||||
|
get data() { return this._data; }
|
||||||
|
get time() { return this._time; }
|
||||||
|
|
||||||
|
static fromResponse(response) {
|
||||||
|
const ret = new Snapshot();
|
||||||
|
ret._updateFromResponse(response);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateFromResponse(response) {
|
||||||
|
const map = {
|
||||||
|
_operation: response.operation,
|
||||||
|
_type: response.type,
|
||||||
|
_id: response.id,
|
||||||
|
_user: response.user,
|
||||||
|
_data: response.data,
|
||||||
|
_time: response.time,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(this, map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Snapshot;
|
25
client/js/models/snapshot_list.js
Normal file
25
client/js/models/snapshot_list.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const api = require('../api.js');
|
||||||
|
const AbstractList = require('./abstract_list.js');
|
||||||
|
const Snapshot = require('./snapshot.js');
|
||||||
|
|
||||||
|
class SnapshotList extends AbstractList {
|
||||||
|
static search(text, page, pageSize) {
|
||||||
|
const url =
|
||||||
|
`/snapshots/?query=${text}` +
|
||||||
|
`&page=${page}` +
|
||||||
|
`&pageSize=${pageSize}`;
|
||||||
|
return api.get(url).then(response => {
|
||||||
|
return Promise.resolve(Object.assign(
|
||||||
|
{},
|
||||||
|
response,
|
||||||
|
{results: SnapshotList.fromResponse(response.results)}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SnapshotList._itemClass = Snapshot;
|
||||||
|
SnapshotList._itemName = 'snapshot';
|
||||||
|
|
||||||
|
module.exports = SnapshotList;
|
|
@ -169,28 +169,36 @@ function getPostEditUrl(id, parameters) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePostLink(id) {
|
function makePostLink(id, includeHash) {
|
||||||
const text = '@' + id;
|
let text = id;
|
||||||
|
if (includeHash) {
|
||||||
|
text = '@' + id;
|
||||||
|
}
|
||||||
return api.hasPrivilege('posts:view') ?
|
return api.hasPrivilege('posts:view') ?
|
||||||
makeNonVoidElement(
|
makeNonVoidElement(
|
||||||
'a', {'href': '/post/' + encodeURIComponent(id)}, text) :
|
'a', {'href': '/post/' + encodeURIComponent(id)}, text) :
|
||||||
text;
|
text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTagLink(name) {
|
function makeTagLink(name, includeHash) {
|
||||||
const tag = tags.getTagByName(name);
|
const tag = tags.getTagByName(name);
|
||||||
const category = tag ? tag.category : 'unknown';
|
const category = tag ? tag.category : 'unknown';
|
||||||
|
let text = name;
|
||||||
|
if (includeHash === true) {
|
||||||
|
text = '#' + text;
|
||||||
|
}
|
||||||
return api.hasPrivilege('tags:view') ?
|
return api.hasPrivilege('tags:view') ?
|
||||||
makeNonVoidElement(
|
makeNonVoidElement(
|
||||||
'a', {
|
'a',
|
||||||
|
{
|
||||||
'href': '/tag/' + encodeURIComponent(name),
|
'href': '/tag/' + encodeURIComponent(name),
|
||||||
'class': misc.makeCssName(category, 'tag'),
|
'class': misc.makeCssName(category, 'tag'),
|
||||||
}, name) :
|
|
||||||
makeNonVoidElement(
|
|
||||||
'span', {
|
|
||||||
'class': misc.makeCssName(category, 'tag'),
|
|
||||||
},
|
},
|
||||||
name);
|
text) :
|
||||||
|
makeNonVoidElement(
|
||||||
|
'span',
|
||||||
|
{'class': misc.makeCssName(category, 'tag')},
|
||||||
|
text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUserLink(user) {
|
function makeUserLink(user) {
|
||||||
|
@ -477,6 +485,8 @@ module.exports = misc.arrayToObject([
|
||||||
decorateValidator,
|
decorateValidator,
|
||||||
makeVoidElement,
|
makeVoidElement,
|
||||||
makeNonVoidElement,
|
makeNonVoidElement,
|
||||||
|
makeTagLink,
|
||||||
|
makePostLink,
|
||||||
syncScrollPosition,
|
syncScrollPosition,
|
||||||
slideDown,
|
slideDown,
|
||||||
slideUp,
|
slideUp,
|
||||||
|
|
131
client/js/views/snapshots_page_view.js
Normal file
131
client/js/views/snapshots_page_view.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const views = require('../util/views.js');
|
||||||
|
|
||||||
|
const template = views.getTemplate('snapshots-page');
|
||||||
|
|
||||||
|
function _extend(target, source) {
|
||||||
|
target.push.apply(target, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatBasicChange(diff, text) {
|
||||||
|
const lines = [];
|
||||||
|
if (diff.type === 'list change') {
|
||||||
|
const addedItems = diff.added;
|
||||||
|
const removedItems = diff.removed;
|
||||||
|
if (addedItems && addedItems.length) {
|
||||||
|
lines.push(`Added ${text} (${addedItems.join(', ')})`);
|
||||||
|
}
|
||||||
|
if (removedItems && removedItems.length) {
|
||||||
|
lines.push(`Removed ${text} (${removedItems.join(', ')})`);
|
||||||
|
}
|
||||||
|
} else if (diff.type === 'primitive change') {
|
||||||
|
const oldValue = diff['old-value'];
|
||||||
|
const newValue = diff['new-value'];
|
||||||
|
lines.push(`Changed ${text} (${oldValue} → ${newValue})`);
|
||||||
|
} else {
|
||||||
|
lines.push(`Changed ${text}`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeResourceLink(type, id) {
|
||||||
|
if (type === 'post') {
|
||||||
|
return views.makePostLink(id, true);
|
||||||
|
} else if (type === 'tag') {
|
||||||
|
return views.makeTagLink(id, true);
|
||||||
|
} else if (type === 'tag_category') {
|
||||||
|
return 'category "' + id + '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeItemCreation(type, data) {
|
||||||
|
const lines = [];
|
||||||
|
for (let key of Object.keys(data)) {
|
||||||
|
if (!data[key]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let text = key[0].toUpperCase() + key.substr(1).toLowerCase();
|
||||||
|
if (Array.isArray(data[key])) {
|
||||||
|
if (data[key].length) {
|
||||||
|
lines.push(`${text}: ${data[key].join(', ')}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(`${text}: ${data[key]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join('<br/>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _makeItemModification(type, data) {
|
||||||
|
const lines = [];
|
||||||
|
const diff = data.value;
|
||||||
|
if (type === 'tag_category') {
|
||||||
|
if (diff.name) {
|
||||||
|
_extend(lines, _formatBasicChange(diff.name, 'name'));
|
||||||
|
}
|
||||||
|
if (diff.color) {
|
||||||
|
_extend(lines, _formatBasicChange(diff.color, 'color'));
|
||||||
|
}
|
||||||
|
if (diff.default) {
|
||||||
|
_extend(lines, ['Made into default category']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (type === 'tag') {
|
||||||
|
if (diff.names) {
|
||||||
|
_extend(lines, _formatBasicChange(diff.names, 'names'));
|
||||||
|
}
|
||||||
|
if (diff.category) {
|
||||||
|
_extend(
|
||||||
|
lines, _formatBasicChange(diff.category, 'category'));
|
||||||
|
}
|
||||||
|
if (diff.suggestions) {
|
||||||
|
_extend(
|
||||||
|
lines, _formatBasicChange(diff.suggestions, 'suggestions'));
|
||||||
|
}
|
||||||
|
if (diff.implications) {
|
||||||
|
_extend(
|
||||||
|
lines, _formatBasicChange(diff.implications, 'implications'));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (type === 'post') {
|
||||||
|
if (diff.checksum) {
|
||||||
|
_extend(lines, ['Changed content']);
|
||||||
|
}
|
||||||
|
if (diff.featured) {
|
||||||
|
_extend(lines, ['Featured on front page']);
|
||||||
|
}
|
||||||
|
if (diff.source) {
|
||||||
|
_extend(lines, _formatBasicChange(diff.source, 'source'));
|
||||||
|
}
|
||||||
|
if (diff.safety) {
|
||||||
|
_extend(lines, _formatBasicChange(diff.safety, 'safety'));
|
||||||
|
}
|
||||||
|
if (diff.tags) {
|
||||||
|
_extend(lines, _formatBasicChange(diff.tags, 'tags'));
|
||||||
|
}
|
||||||
|
if (diff.relations) {
|
||||||
|
_extend(lines, _formatBasicChange(diff.relations, 'relations'));
|
||||||
|
}
|
||||||
|
if (diff.notes) {
|
||||||
|
_extend(lines, ['Changed notes']);
|
||||||
|
}
|
||||||
|
if (diff.flags) {
|
||||||
|
_extend(lines, ['Changed flags']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('<br/>');
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnapshotsPageView {
|
||||||
|
constructor(ctx) {
|
||||||
|
views.replaceContent(ctx.hostNode, template(Object.assign({
|
||||||
|
makeResourceLink: _makeResourceLink,
|
||||||
|
makeItemCreation: _makeItemCreation,
|
||||||
|
makeItemModification: _makeItemModification,
|
||||||
|
}, ctx)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SnapshotsPageView;
|
Loading…
Reference in a new issue