334 lines
8.9 KiB
Vue
334 lines
8.9 KiB
Vue
<template>
|
|
<div class="history side-bar__panel">
|
|
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
|
|
<div class="history__spacer" v-if="revision.spacer"></div>
|
|
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
|
|
<div class="revision__icon">
|
|
<user-image :user-id="revision.sub"></user-image>
|
|
</div>
|
|
<div class="revision__header flex flex--column">
|
|
<user-name :user-id="revision.sub"></user-name>
|
|
<div class="revision__created">{{revision.created | formatTime}}</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<div class="history__spacer history__spacer--last" v-if="revisions.length"></div>
|
|
<div class="flex flex--row flex--end" v-if="showMoreButton">
|
|
<button class="history__button button" @click="showMore">More</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapMutations } from 'vuex';
|
|
import providerRegistry from '../../services/providers/providerRegistry';
|
|
import MenuEntry from './common/MenuEntry';
|
|
import UserImage from '../UserImage';
|
|
import UserName from '../UserName';
|
|
import EditorClassApplier from '../common/EditorClassApplier';
|
|
import PreviewClassApplier from '../common/PreviewClassApplier';
|
|
import utils from '../../services/utils';
|
|
import editorSvc from '../../services/editorSvc';
|
|
|
|
let editorClassAppliers = [];
|
|
let previewClassAppliers = [];
|
|
|
|
let cachedFileId;
|
|
let revisionsPromise;
|
|
let revisionContentPromises;
|
|
const pageSize = 30;
|
|
const spacerThreshold = 12 * 60 * 60 * 1000; // 12h
|
|
|
|
export default {
|
|
components: {
|
|
MenuEntry,
|
|
UserImage,
|
|
UserName,
|
|
},
|
|
data: () => ({
|
|
allRevisions: [],
|
|
showCount: pageSize,
|
|
}),
|
|
computed: {
|
|
revisions() {
|
|
return this.allRevisions.slice(0, this.showCount);
|
|
},
|
|
revisionsWithSpacer() {
|
|
let previousCreated = 0;
|
|
return this.revisions.map((revision) => {
|
|
const revisionWithSpacer = {
|
|
...revision,
|
|
spacer: revision.created + spacerThreshold < previousCreated,
|
|
};
|
|
previousCreated = revision.created;
|
|
return revisionWithSpacer;
|
|
});
|
|
},
|
|
showMoreButton() {
|
|
return this.showCount < this.allRevisions.length;
|
|
},
|
|
},
|
|
methods: {
|
|
...mapMutations('content', [
|
|
'setRevisionContent',
|
|
]),
|
|
close() {
|
|
this.$store.dispatch('data/setSideBarPanel', 'menu');
|
|
},
|
|
showMore() {
|
|
this.showCount += pageSize;
|
|
},
|
|
open(revision) {
|
|
let revisionContentPromise = revisionContentPromises[revision.id];
|
|
if (!revisionContentPromise) {
|
|
revisionContentPromise = new Promise((resolve, reject) => {
|
|
const syncToken = this.$store.getters['workspace/syncToken'];
|
|
const currentFile = this.$store.getters['file/current'];
|
|
this.$store.dispatch('queue/enqueue',
|
|
() => Promise.resolve()
|
|
.then(() => this.workspaceProvider.getRevisionContent(
|
|
syncToken, currentFile.id, revision.id))
|
|
.then(resolve, reject));
|
|
});
|
|
revisionContentPromises[revision.id] = revisionContentPromise;
|
|
revisionContentPromise.catch(() => {
|
|
revisionContentPromises[revision.id] = null;
|
|
});
|
|
}
|
|
revisionContentPromise.then(revisionContent =>
|
|
this.$store.dispatch('content/setRevisionContent', revisionContent));
|
|
},
|
|
refreshHighlighters() {
|
|
const revisionContent = this.$store.state.content.revisionContent;
|
|
editorClassAppliers.forEach(editorClassApplier => editorClassApplier.stop());
|
|
editorClassAppliers = [];
|
|
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
|
|
previewClassAppliers = [];
|
|
if (revisionContent) {
|
|
editorSvc.$once('sectionDescWithDiffsList', () => {
|
|
let offset = 0;
|
|
revisionContent.diffs.forEach(([type, text]) => {
|
|
if (type) {
|
|
const classes = ['revision-diff', `revision-diff--${type > 0 ? 'insert' : 'delete'}`];
|
|
const offsets = {
|
|
start: offset,
|
|
end: offset + text.length,
|
|
};
|
|
editorClassAppliers.push(new EditorClassApplier(
|
|
[`revision-diff--${utils.uid()}`, ...classes], offsets));
|
|
previewClassAppliers.push(new PreviewClassApplier(
|
|
[`revision-diff--${utils.uid()}`, ...classes], offsets));
|
|
}
|
|
offset += text.length;
|
|
});
|
|
});
|
|
}
|
|
},
|
|
},
|
|
created() {
|
|
// Find the workspace provider
|
|
const workspace = this.$store.getters['workspace/currentWorkspace'];
|
|
this.workspaceProvider = providerRegistry.providers[workspace.providerId];
|
|
|
|
// Watch file changes
|
|
this.$watch(
|
|
() => this.$store.getters['file/current'].id,
|
|
(id) => {
|
|
this.allRevisions = [];
|
|
if (id) {
|
|
if (id !== cachedFileId) {
|
|
this.setRevisionContent();
|
|
cachedFileId = id;
|
|
revisionContentPromises = {};
|
|
const syncToken = this.$store.getters['workspace/syncToken'];
|
|
const currentFile = this.$store.getters['file/current'];
|
|
revisionsPromise = new Promise((resolve, reject) => {
|
|
this.$store.dispatch('queue/enqueue',
|
|
() => Promise.resolve()
|
|
.then(() => this.workspaceProvider.listRevisions(syncToken, currentFile.id))
|
|
.then(resolve, reject));
|
|
});
|
|
revisionsPromise.catch(() => {
|
|
cachedFileId = null;
|
|
return [];
|
|
});
|
|
}
|
|
revisionsPromise.then((revisions) => {
|
|
this.allRevisions = revisions;
|
|
});
|
|
}
|
|
}, { immediate: true });
|
|
|
|
const loadOne = () => {
|
|
if (!this.destroyed) {
|
|
this.$store.dispatch('queue/enqueue',
|
|
() => {
|
|
let loadPromise;
|
|
this.revisions.some((revision) => {
|
|
if (!revision.created) {
|
|
const syncToken = this.$store.getters['workspace/syncToken'];
|
|
const currentFile = this.$store.getters['file/current'];
|
|
loadPromise = this.workspaceProvider
|
|
.loadRevision(syncToken, currentFile.id, revision)
|
|
.then(() => loadOne());
|
|
}
|
|
return loadPromise;
|
|
});
|
|
return loadPromise;
|
|
});
|
|
}
|
|
};
|
|
|
|
this.$watch(
|
|
() => this.revisions,
|
|
() => loadOne(),
|
|
{ immediate: true });
|
|
|
|
// Watch diffs changes
|
|
this.$watch(
|
|
() => this.$store.state.content.revisionContent,
|
|
() => this.refreshHighlighters());
|
|
|
|
// Close revision
|
|
this.onKeyup = (evt) => {
|
|
if (evt.which === 27) {
|
|
// Esc key
|
|
this.setRevisionContent();
|
|
}
|
|
};
|
|
window.addEventListener('keyup', this.onKeyup);
|
|
},
|
|
destroyed() {
|
|
// Close revision
|
|
this.setRevisionContent();
|
|
// Remove highlighters
|
|
this.refreshHighlighters();
|
|
// Remove event listener
|
|
window.removeEventListener('keyup', this.onKeyup);
|
|
// Cancel loading revisions
|
|
this.destroyed = true;
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@import '../common/variables.scss';
|
|
|
|
.history {
|
|
padding: 5px 5px 50px;
|
|
}
|
|
|
|
.history__button {
|
|
font-size: 14px;
|
|
margin-top: 0.5em;
|
|
}
|
|
|
|
.history__spacer {
|
|
position: relative;
|
|
height: 40px;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
height: 100%;
|
|
top: 0;
|
|
left: 24px;
|
|
border-left: 2px dotted $hr-color;
|
|
}
|
|
}
|
|
|
|
.history__spacer--last {
|
|
height: 20px;
|
|
}
|
|
|
|
.revision__button {
|
|
text-align: left;
|
|
padding: 15px;
|
|
height: auto;
|
|
text-transform: none;
|
|
position: relative;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
height: 100%;
|
|
top: 0;
|
|
left: 24px;
|
|
border-left: 2px solid $hr-color;
|
|
}
|
|
|
|
&:active,
|
|
&:focus,
|
|
&:hover {
|
|
&::before {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.revision:first-child &::before {
|
|
height: 67%;
|
|
top: 33%;
|
|
}
|
|
}
|
|
|
|
.revision__icon {
|
|
height: 20px;
|
|
width: 20px;
|
|
margin-right: 12px;
|
|
flex: none;
|
|
border-radius: $border-radius-base;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.revision__header {
|
|
font-size: 15px;
|
|
width: 100%;
|
|
}
|
|
|
|
.revision__created {
|
|
font-size: 0.75em;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.layout--revision {
|
|
.cledit-section *,
|
|
.cl-preview-section * {
|
|
color: transparentize($editor-color-light, 0.67) !important;
|
|
|
|
.app--dark & {
|
|
color: transparentize($editor-color-dark, 0.67) !important;
|
|
}
|
|
}
|
|
|
|
.cledit-section .revision-diff {
|
|
color: $editor-color-light !important;
|
|
|
|
.app--dark & {
|
|
color: $editor-color-dark !important;
|
|
}
|
|
}
|
|
|
|
.cl-preview-section .revision-diff {
|
|
color: $body-color-light !important;
|
|
|
|
.app--dark & {
|
|
color: $body-color-dark !important;
|
|
}
|
|
}
|
|
|
|
.revision-diff {
|
|
padding: 0.25em 0;
|
|
|
|
&.revision-diff--insert {
|
|
background-color: mix(#fff, $selection-highlighting-color, 60%);
|
|
}
|
|
|
|
&.revision-diff--delete {
|
|
background-color: mix(#fff, $error-color, 60%);
|
|
text-decoration: line-through;
|
|
}
|
|
}
|
|
}
|
|
</style>
|