2017-01-31 07:50:28 +01:00
|
|
|
const Backbone = require('backbone');
|
|
|
|
const KeyHandler = require('../../comp/key-handler');
|
|
|
|
const Keys = require('../../const/keys');
|
|
|
|
const Format = require('../../util/format');
|
|
|
|
const Locale = require('../../util/locale');
|
|
|
|
const Alerts = require('../../comp/alerts');
|
|
|
|
const FieldViewReadOnly = require('../fields/field-view-read-only');
|
|
|
|
const FieldViewReadOnlyRaw = require('../fields/field-view-read-only-raw');
|
2019-01-02 10:49:12 +01:00
|
|
|
const Copyable = require('../../mixins/copyable');
|
2017-01-31 07:50:28 +01:00
|
|
|
|
|
|
|
const DetailsHistoryView = Backbone.View.extend({
|
2015-12-16 22:50:45 +01:00
|
|
|
template: require('templates/details/details-history.hbs'),
|
2015-10-17 23:49:24 +02:00
|
|
|
|
|
|
|
events: {
|
|
|
|
'click .details__history-close': 'closeHistory',
|
|
|
|
'click .details__history-timeline-item': 'timelineItemClick',
|
|
|
|
'click .details__history-arrow-prev': 'timelinePrevClick',
|
|
|
|
'click .details__history-arrow-next': 'timelineNextClick',
|
|
|
|
'click .details__history-button-revert': 'revertClick',
|
|
|
|
'click .details__history-button-delete': 'deleteClick',
|
|
|
|
'click .details__history-button-discard': 'discardClick'
|
|
|
|
},
|
|
|
|
|
|
|
|
formats: [
|
2019-08-16 23:05:39 +02:00
|
|
|
{
|
|
|
|
name: 'ms',
|
|
|
|
round: 1,
|
2019-08-18 10:17:09 +02:00
|
|
|
format(d) {
|
2019-08-16 23:05:39 +02:00
|
|
|
return Format.dtStr(d);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'sec',
|
|
|
|
round: 1000,
|
2019-08-18 10:17:09 +02:00
|
|
|
format(d) {
|
2019-08-16 23:05:39 +02:00
|
|
|
return Format.dtStr(d);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'min',
|
|
|
|
round: 1000 * 60,
|
2019-08-18 10:17:09 +02:00
|
|
|
format(d) {
|
2019-08-16 23:05:39 +02:00
|
|
|
return Format.dtStr(d).replace(':00 ', ' ');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'hour',
|
|
|
|
round: 1000 * 60 * 60,
|
2019-08-18 10:17:09 +02:00
|
|
|
format(d) {
|
2019-08-16 23:05:39 +02:00
|
|
|
return Format.dtStr(d).replace(':00', '');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'day',
|
|
|
|
round: 1000 * 60 * 60 * 24,
|
2019-08-18 10:17:09 +02:00
|
|
|
format(d) {
|
2019-08-16 23:05:39 +02:00
|
|
|
return Format.dStr(d);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'month',
|
|
|
|
round: 1000 * 60 * 60 * 24 * 31,
|
2019-08-18 10:17:09 +02:00
|
|
|
format(d) {
|
2019-08-16 23:05:39 +02:00
|
|
|
return Format.dStr(d);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'year',
|
|
|
|
round: 1000 * 60 * 60 * 24 * 365,
|
2019-08-18 10:17:09 +02:00
|
|
|
format(d) {
|
2019-08-16 23:05:39 +02:00
|
|
|
return d.getFullYear();
|
|
|
|
}
|
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
],
|
|
|
|
|
|
|
|
fieldViews: null,
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
initialize() {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.fieldViews = [];
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
render(visibleRecord) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.renderTemplate(null, true);
|
|
|
|
KeyHandler.onKey(Keys.DOM_VK_ESCAPE, this.closeHistory, this);
|
|
|
|
this.history = this.model.getHistory();
|
|
|
|
this.buildTimeline();
|
|
|
|
this.timelineEl = this.$el.find('.details__history-timeline');
|
|
|
|
this.bodyEl = this.$el.find('.details__history-body');
|
|
|
|
this.timeline.forEach(function(item, ix) {
|
2019-08-16 23:05:39 +02:00
|
|
|
$('<i/>')
|
|
|
|
.addClass('fa fa-circle details__history-timeline-item')
|
|
|
|
.css('left', item.pos * 100 + '%')
|
2015-10-17 23:49:24 +02:00
|
|
|
.attr('data-id', ix)
|
|
|
|
.appendTo(this.timelineEl);
|
|
|
|
}, this);
|
|
|
|
this.labels.forEach(function(label) {
|
2019-08-16 23:05:39 +02:00
|
|
|
$('<div/>')
|
|
|
|
.addClass('details__history-timeline-label')
|
|
|
|
.css('left', label.pos * 100 + '%')
|
2015-10-17 23:49:24 +02:00
|
|
|
.text(label.text)
|
|
|
|
.appendTo(this.timelineEl);
|
|
|
|
}, this);
|
|
|
|
if (visibleRecord === undefined) {
|
|
|
|
visibleRecord = this.history.length - 1;
|
|
|
|
}
|
|
|
|
this.showRecord(visibleRecord);
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
remove() {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.removeFieldViews();
|
|
|
|
KeyHandler.offKey(Keys.DOM_VK_ESCAPE, this.closeHistory, this);
|
|
|
|
Backbone.View.prototype.remove.call(this);
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
removeFieldViews() {
|
2016-07-17 13:30:38 +02:00
|
|
|
this.fieldViews.forEach(fieldView => fieldView.remove());
|
2015-10-17 23:49:24 +02:00
|
|
|
this.fieldViews = [];
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
showRecord(ix) {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.activeIx = ix;
|
|
|
|
this.record = this.timeline[ix].rec;
|
2019-08-18 08:05:38 +02:00
|
|
|
this.timelineEl
|
|
|
|
.find('.details__history-timeline-item')
|
|
|
|
.removeClass('details__history-timeline-item--active');
|
2019-08-16 23:05:39 +02:00
|
|
|
this.timelineEl
|
|
|
|
.find('.details__history-timeline-item[data-id="' + ix + '"]')
|
|
|
|
.addClass('details__history-timeline-item--active');
|
2015-10-17 23:49:24 +02:00
|
|
|
this.removeFieldViews();
|
|
|
|
this.bodyEl.html('');
|
2017-01-31 07:50:28 +01:00
|
|
|
const colorCls = this.record.color ? this.record.color + '-color' : '';
|
2019-08-16 23:05:39 +02:00
|
|
|
this.fieldViews.push(
|
2019-08-18 08:05:38 +02:00
|
|
|
new FieldViewReadOnly({
|
|
|
|
model: { name: 'Rev', title: Locale.detHistoryVersion, value: ix + 1 }
|
|
|
|
})
|
2019-08-16 23:05:39 +02:00
|
|
|
);
|
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnly({
|
|
|
|
model: {
|
|
|
|
name: 'Updated',
|
|
|
|
title: Locale.detHistorySaved,
|
|
|
|
value:
|
|
|
|
Format.dtStr(this.record.updated) +
|
|
|
|
(this.record.unsaved ? ' (' + Locale.detHistoryCurUnsavedState + ')' : '') +
|
|
|
|
(ix === this.history.length - 1 && !this.record.unsaved
|
|
|
|
? ' (' + Locale.detHistoryCurState + ')'
|
|
|
|
: '')
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnlyRaw({
|
|
|
|
model: {
|
|
|
|
name: '$Title',
|
|
|
|
title: Format.capFirst(Locale.title),
|
|
|
|
value:
|
|
|
|
'<i class="fa fa-' +
|
|
|
|
this.record.icon +
|
|
|
|
' ' +
|
|
|
|
colorCls +
|
|
|
|
'"></i> ' +
|
|
|
|
_.escape(this.record.title) || '(' + Locale.detHistoryNoTitle + ')'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnly({
|
2019-08-18 08:05:38 +02:00
|
|
|
model: {
|
|
|
|
name: '$UserName',
|
|
|
|
title: Format.capFirst(Locale.user),
|
|
|
|
value: this.record.user
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
})
|
|
|
|
);
|
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnly({
|
2019-08-18 08:05:38 +02:00
|
|
|
model: {
|
|
|
|
name: '$Password',
|
|
|
|
title: Format.capFirst(Locale.password),
|
|
|
|
value: this.record.password
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
})
|
|
|
|
);
|
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnly({
|
2019-08-18 08:05:38 +02:00
|
|
|
model: {
|
|
|
|
name: '$URL',
|
|
|
|
title: Format.capFirst(Locale.website),
|
|
|
|
value: this.record.url
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
})
|
|
|
|
);
|
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnly({
|
2019-08-18 08:05:38 +02:00
|
|
|
model: {
|
|
|
|
name: '$Notes',
|
|
|
|
title: Format.capFirst(Locale.notes),
|
|
|
|
value: this.record.notes
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
})
|
|
|
|
);
|
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnly({
|
2019-08-18 08:05:38 +02:00
|
|
|
model: {
|
|
|
|
name: 'Tags',
|
|
|
|
title: Format.capFirst(Locale.tags),
|
|
|
|
value: this.record.tags.join(', ')
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
})
|
|
|
|
);
|
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnly({
|
|
|
|
model: {
|
|
|
|
name: 'Expires',
|
|
|
|
title: Locale.detExpires,
|
|
|
|
value: this.record.expires ? Format.dtStr(this.record.expires) : ''
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
_.forEach(
|
|
|
|
this.record.fields,
|
|
|
|
function(value, field) {
|
|
|
|
this.fieldViews.push(
|
2019-08-18 08:05:38 +02:00
|
|
|
new FieldViewReadOnly({
|
2019-08-18 10:17:09 +02:00
|
|
|
model: { name: '$' + field, title: field, value }
|
2019-08-18 08:05:38 +02:00
|
|
|
})
|
2019-08-16 23:05:39 +02:00
|
|
|
);
|
|
|
|
},
|
|
|
|
this
|
|
|
|
);
|
2015-10-17 23:49:24 +02:00
|
|
|
if (this.record.attachments.length) {
|
2019-08-16 23:05:39 +02:00
|
|
|
this.fieldViews.push(
|
|
|
|
new FieldViewReadOnly({
|
|
|
|
model: {
|
|
|
|
name: 'Attachments',
|
|
|
|
title: Locale.detAttachments,
|
|
|
|
value: this.record.attachments.map(att => att.title).join(', ')
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
2015-10-17 23:49:24 +02:00
|
|
|
}
|
|
|
|
this.fieldViews.forEach(function(fieldView) {
|
|
|
|
fieldView.setElement(this.bodyEl).render();
|
2018-12-28 12:23:20 +01:00
|
|
|
fieldView.on('copy', this.fieldCopied.bind(this));
|
2015-10-17 23:49:24 +02:00
|
|
|
}, this);
|
2017-01-31 07:50:28 +01:00
|
|
|
const buttons = this.$el.find('.details__history-buttons');
|
2015-10-17 23:49:24 +02:00
|
|
|
buttons.find('.details__history-button-revert').toggle(ix < this.history.length - 1);
|
|
|
|
buttons.find('.details__history-button-delete').toggle(ix < this.history.length - 1);
|
2019-08-16 23:05:39 +02:00
|
|
|
buttons
|
|
|
|
.find('.details__history-button-discard')
|
2019-08-18 08:05:38 +02:00
|
|
|
.toggle(
|
|
|
|
(this.record.unsaved &&
|
|
|
|
ix === this.history.length - 1 &&
|
|
|
|
this.history.length > 1) ||
|
|
|
|
false
|
|
|
|
);
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
timelineItemClick(e) {
|
2019-08-16 23:05:39 +02:00
|
|
|
const id = $(e.target)
|
|
|
|
.closest('.details__history-timeline-item')
|
|
|
|
.data('id');
|
2015-10-17 23:49:24 +02:00
|
|
|
this.showRecord(id);
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
timelinePrevClick() {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (this.activeIx > 0) {
|
|
|
|
this.showRecord(this.activeIx - 1);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
timelineNextClick() {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (this.activeIx < this.timeline.length - 1) {
|
|
|
|
this.showRecord(this.activeIx + 1);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
buildTimeline() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const firstRec = this.history[0];
|
|
|
|
const lastRec = this.history[this.history.length - 1];
|
2016-07-17 13:30:38 +02:00
|
|
|
this.timeline = this.history.map(rec => ({
|
|
|
|
pos: (rec.updated - firstRec.updated) / (lastRec.updated - firstRec.updated),
|
2019-08-18 10:17:09 +02:00
|
|
|
rec
|
2016-07-17 13:30:38 +02:00
|
|
|
}));
|
2017-01-31 07:50:28 +01:00
|
|
|
const period = lastRec.updated - firstRec.updated;
|
|
|
|
const format = this.getDateFormat(period);
|
2019-08-18 08:05:38 +02:00
|
|
|
this.labels = this.getLabels(
|
|
|
|
firstRec.updated.getTime(),
|
|
|
|
lastRec.updated.getTime(),
|
|
|
|
format.round
|
|
|
|
).map(label => ({
|
|
|
|
pos: (label - firstRec.updated) / (lastRec.updated - firstRec.updated),
|
|
|
|
val: label,
|
|
|
|
text: format.format(new Date(label))
|
|
|
|
}));
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
getDateFormat(period) {
|
2017-01-31 07:50:28 +01:00
|
|
|
for (let i = 0; i < this.formats.length; i++) {
|
2015-10-17 23:49:24 +02:00
|
|
|
if (period < this.formats[i].round * 1.2) {
|
|
|
|
return this.formats[i > 0 ? i - 1 : 0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return this.formats[this.formats.length - 1];
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
getLabels(first, last, round) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const count = Math.floor((last - first) / round);
|
2015-10-17 23:49:24 +02:00
|
|
|
if (count > 2) {
|
|
|
|
round *= Math.ceil(count / 2);
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const labels = [];
|
|
|
|
let label = Math.ceil(first / round) * round;
|
2015-10-17 23:49:24 +02:00
|
|
|
while (label < last) {
|
|
|
|
labels.push(label);
|
|
|
|
label += round;
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
if (labels.length > 1 && (labels[0] - first) / (last - first) < 0.1) {
|
2015-10-17 23:49:24 +02:00
|
|
|
labels.shift();
|
|
|
|
}
|
|
|
|
return labels;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
closeHistory(updated) {
|
|
|
|
this.trigger('close', { updated });
|
2015-10-17 23:49:24 +02:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
revertClick() {
|
2015-10-17 23:49:24 +02:00
|
|
|
Alerts.yesno({
|
2017-12-17 18:22:20 +01:00
|
|
|
header: Locale.detHistoryRevertAlert,
|
|
|
|
body: Locale.detHistoryRevertAlertBody,
|
2016-07-17 13:30:38 +02:00
|
|
|
success: () => {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.model.revertToHistoryState(this.record.entry);
|
|
|
|
this.closeHistory(true);
|
2016-07-17 13:30:38 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
deleteClick() {
|
2015-10-17 23:49:24 +02:00
|
|
|
Alerts.yesno({
|
2017-12-17 18:22:20 +01:00
|
|
|
header: Locale.detHistoryDeleteAlert,
|
|
|
|
body: Locale.detHistoryDeleteAlertBody,
|
2016-07-17 13:30:38 +02:00
|
|
|
success: () => {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.model.deleteHistory(this.record.entry);
|
|
|
|
this.render(this.activeIx);
|
2016-07-17 13:30:38 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
discardClick() {
|
2015-10-17 23:49:24 +02:00
|
|
|
Alerts.yesno({
|
2017-12-17 18:22:20 +01:00
|
|
|
header: Locale.detHistoryDiscardChangesAlert,
|
|
|
|
body: Locale.detHistoryDiscardChangesAlertBody,
|
2016-07-17 13:30:38 +02:00
|
|
|
success: () => {
|
2015-10-17 23:49:24 +02:00
|
|
|
this.model.discardUnsaved();
|
|
|
|
this.closeHistory(true);
|
2016-07-17 13:30:38 +02:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
});
|
2019-01-02 10:49:12 +01:00
|
|
|
}
|
2015-10-17 23:49:24 +02:00
|
|
|
});
|
|
|
|
|
2019-01-02 10:49:12 +01:00
|
|
|
_.extend(DetailsHistoryView.prototype, Copyable);
|
|
|
|
|
2015-10-17 23:49:24 +02:00
|
|
|
module.exports = DetailsHistoryView;
|