output revisions for pages
This commit is contained in:
@@ -71,18 +71,6 @@ namespace zero.Core.Api
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ListResult<T>> GetRevisions(string id, int pageNumber = 1, int pageSize = 30)
|
||||
{
|
||||
using (IAsyncDocumentSession session = Raven.OpenAsyncSession())
|
||||
{
|
||||
|
||||
List<T> revisions = await session.Advanced.Revisions.GetForAsync<T>(id, pageNumber - 1, pageSize);
|
||||
return new ListResult<T>(revisions, revisions.Count, pageNumber, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<EntityResult<T>> Save(T model)
|
||||
{
|
||||
@@ -310,11 +298,6 @@ namespace zero.Core.Api
|
||||
/// </summary>
|
||||
PageType GetPageType(string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Get revisions of a page (if activated in RavenDB configuration)
|
||||
/// </summary>
|
||||
Task<ListResult<T>> GetRevisions(string id, int pageNumber = 1, int pageSize = 30);
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates a page
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using Newtonsoft.Json;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero.Core.Api
|
||||
{
|
||||
public class RevisionsApi : IRevisionsApi
|
||||
{
|
||||
protected IZeroOptions Options { get; set; }
|
||||
|
||||
protected IDocumentStore Raven { get; set; }
|
||||
|
||||
|
||||
public RevisionsApi(IZeroOptions options, IDocumentStore raven)
|
||||
{
|
||||
Options = options;
|
||||
Raven = raven;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get revision list for an entity
|
||||
/// </summary>
|
||||
public async Task<ListResult<Revision>> GetPaged<T>(string id, int pageNumber = 1, int pageSize = 30, bool includeContent = false) where T : IZeroEntity
|
||||
{
|
||||
using IAsyncDocumentSession session = Raven.OpenAsyncSession();
|
||||
|
||||
List<T> items = await session.Advanced.Revisions.GetForAsync<T>(id, pageNumber - 1, pageSize);
|
||||
List<Revision> revisions = new List<Revision>();
|
||||
|
||||
string[] userIds = items.Select(x => x.LastModifiedById).Distinct().ToArray();
|
||||
Dictionary<string, User> users = await session.LoadAsync<User>(userIds);
|
||||
|
||||
foreach (T item in items)
|
||||
{
|
||||
DateTime? date = session.Advanced.GetLastModifiedFor(item);
|
||||
|
||||
Revision revision = new Revision()
|
||||
{
|
||||
ChangeVector = session.Advanced.GetChangeVectorFor(item),
|
||||
Date = date.HasValue ? new DateTimeOffset(date.Value) : item.CreatedDate,
|
||||
Json = includeContent ? JsonConvert.SerializeObject(item) : null
|
||||
};
|
||||
|
||||
if (!item.LastModifiedById.IsNullOrEmpty() && users.TryGetValue(item.LastModifiedById, out User user))
|
||||
{
|
||||
revision.User = new RevisionUser()
|
||||
{
|
||||
AvatarId = user.AvatarId,
|
||||
Id = user.Id,
|
||||
Name = user.Name
|
||||
};
|
||||
}
|
||||
|
||||
revisions.Add(revision);
|
||||
}
|
||||
|
||||
return new ListResult<Revision>(revisions, revisions.Count, pageNumber, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IRevisionsApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Get revision list (without content JSON) for an entity
|
||||
/// </summary>
|
||||
Task<ListResult<Revision>> GetPaged<T>(string id, int pageNumber = 1, int pageSize = 30, bool includeContent = false) where T : IZeroEntity;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,24 @@
|
||||
//using System;
|
||||
using System;
|
||||
|
||||
//namespace zero.Core.Entities
|
||||
//{
|
||||
// public class Revision : ZeroEntity, IRevision
|
||||
// {
|
||||
// /// <inheritdoc />
|
||||
// public string Content { get; set; }
|
||||
// }
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class Revision
|
||||
{
|
||||
public RevisionUser User { get; set; }
|
||||
|
||||
public DateTimeOffset Date { get; set; }
|
||||
|
||||
// public interface IRevision : IZeroEntity, IZeroDbConventions
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// Id of the affected entity
|
||||
// /// </summary>
|
||||
// string EntityId { get; set; }
|
||||
public string ChangeVector { get; set; }
|
||||
|
||||
// /// <summary>
|
||||
// /// Id of the affected entity
|
||||
// /// </summary>
|
||||
// string EntityId { get; set; }
|
||||
public string Json { get; set; }
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// Contains the content as JSON
|
||||
// /// </summary>
|
||||
// string Content { get; set; }
|
||||
// }
|
||||
//}
|
||||
public class RevisionUser
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string AvatarId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class RavenDocumentSessionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get revision list (without content JSON) for an entity
|
||||
/// </summary>
|
||||
public static async Task<ListResult<Revision>> GetRevisions<T>(this IAsyncDocumentSession session, string id, int pageNumber = 1, int pageSize = 30) where T : IZeroEntity
|
||||
{
|
||||
List<T> items = await session.Advanced.Revisions.GetForAsync<T>(id, pageNumber - 1, pageSize);
|
||||
List<Revision> revisions = new List<Revision>();
|
||||
|
||||
string[] userIds = items.Select(x => x.LastModifiedById).Distinct().ToArray();
|
||||
Dictionary<string, User> users = await session.LoadAsync<User>(userIds);
|
||||
|
||||
foreach (T item in items)
|
||||
{
|
||||
DateTime? date = session.Advanced.GetLastModifiedFor(item);
|
||||
|
||||
Revision revision = new Revision()
|
||||
{
|
||||
ChangeVector = session.Advanced.GetChangeVectorFor(item),
|
||||
Date = date.HasValue ? new DateTimeOffset(date.Value) : item.CreatedDate
|
||||
};
|
||||
|
||||
if (!item.LastModifiedById.IsNullOrEmpty() && users.TryGetValue(item.LastModifiedById, out User user))
|
||||
{
|
||||
revision.User = new RevisionUser()
|
||||
{
|
||||
AvatarId = user.AvatarId,
|
||||
Id = user.Id,
|
||||
Name = user.Name
|
||||
};
|
||||
}
|
||||
|
||||
revisions.Add(revision);
|
||||
}
|
||||
|
||||
return new ListResult<Revision>(revisions, revisions.Count, pageNumber, pageSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -12,7 +12,7 @@ using zero.Core.Utils;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class DocumentStoreExtensions
|
||||
public static class RavenDocumentStoreExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Setup conventions for the document store
|
||||
@@ -18,6 +18,10 @@
|
||||
format: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
split: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,10 +51,24 @@
|
||||
return;
|
||||
}
|
||||
|
||||
this.output = Strings.date(this.value, this.format);
|
||||
if (!this.split)
|
||||
{
|
||||
this.output = Strings.date(this.value, this.format);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.output = Strings.date(this.value, 'short') + ' <span class="-minor">' + Strings.date(this.value, 'time') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-date .-minor
|
||||
{
|
||||
color: var(--color-fg-dim);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="ui-revisions" :class="{ 'is-empty': value.items.length < 1 }">
|
||||
<div class="ui-revision" v-for="revision in value.items">
|
||||
<span class="ui-revision-action">updated</span>
|
||||
<ui-date class="ui-revision-date" v-model="revision.date" format="long" :split="true" />
|
||||
<router-link :to="{ name: userRoute, params: { id: revision.user.id }}" v-if="revision.user" class="ui-revision-user">
|
||||
<img class="ui-revision-user-image" v-if="revision.user" :src="getImage(revision.user.avatarId)" :alt="revision.user.name" />
|
||||
<span v-if="revision.user" class="ui-revision-user-name">{{revision.user.name}}</span>
|
||||
</router-link>
|
||||
<div v-else></div>
|
||||
<button class="ui-link">View</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import dayjs from 'dayjs';
|
||||
import Strings from 'zero/services/strings';
|
||||
import MediaApi from 'zero/resources/media.js';
|
||||
|
||||
export default {
|
||||
name: 'uiRevisions',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
totalItems: 0,
|
||||
items: []
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
data: () => ({
|
||||
userRoute: zero.alias.sections.settings + '-' + zero.alias.settings.users + '-edit'
|
||||
}),
|
||||
|
||||
|
||||
methods: {
|
||||
getImage(id)
|
||||
{
|
||||
return MediaApi.getImageSource(id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.ui-revision
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto 3fr 2fr auto;
|
||||
grid-gap: 20px;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
|
||||
& + .ui-revision
|
||||
{
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&:first-child .ui-revision-action:before
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-revision-action
|
||||
{
|
||||
align-self: center;
|
||||
display: inline-block;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-accent-info-bg);
|
||||
color: var(--color-accent-info);
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding: 0 10px;
|
||||
border-radius: 16px;
|
||||
letter-spacing: .5px;
|
||||
position: relative;
|
||||
|
||||
&:before
|
||||
{
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: calc(50% - 1.5px);
|
||||
top: -30px;
|
||||
height: 30px;
|
||||
width: 3px;
|
||||
background: var(--color-accent-info-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-revision-user
|
||||
{
|
||||
color: var(--color-fg);
|
||||
}
|
||||
|
||||
.ui-revision-user-image
|
||||
{
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 14px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="page-editor-info ui-view-box has-sidebar">
|
||||
<div class="ui-box">
|
||||
box
|
||||
<div>
|
||||
<div class="ui-box"></div>
|
||||
<div class="ui-box">
|
||||
<h3 class="ui-headline">Revisions</h3>
|
||||
<ui-revisions v-model="revisions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui-view-box-aside editor-infos">
|
||||
<div class="ui-box editor-active-toggle" :class="{'is-active': value.isActive }">
|
||||
@@ -13,10 +17,10 @@
|
||||
</ui-property>
|
||||
</div>
|
||||
<div class="ui-box">
|
||||
<ui-property v-if="value.id" label="@ui.id" :is-text="true">
|
||||
<ui-property v-if="!isCreate" label="@ui.id" :is-text="true">
|
||||
{{value.id}}
|
||||
</ui-property>
|
||||
<ui-property v-if="value.id" label="@ui.createdDate" :is-text="true">
|
||||
<ui-property v-if="!isCreate" label="@ui.createdDate" :is-text="true">
|
||||
<ui-date v-model="value.createdDate" />
|
||||
</ui-property>
|
||||
<ui-property label="Type" :is-text="true" v-if="pageType">
|
||||
@@ -39,15 +43,34 @@
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
pageType: null
|
||||
pageType: null,
|
||||
revisions: {
|
||||
totalItems: 0,
|
||||
items: []
|
||||
}
|
||||
}),
|
||||
|
||||
computed: {
|
||||
isCreate()
|
||||
{
|
||||
return !this.value.id;
|
||||
}
|
||||
},
|
||||
|
||||
mounted()
|
||||
{
|
||||
PagesApi.getPageType(this.value.pageTypeAlias).then(pageType =>
|
||||
{
|
||||
this.pageType = pageType;
|
||||
});
|
||||
|
||||
if (!this.isCreate)
|
||||
{
|
||||
PagesApi.getRevisions(this.value.id).then(response =>
|
||||
{
|
||||
this.revisions = response;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
</ui-form-header>
|
||||
|
||||
<ui-editor v-if="!loading" :config="renderer" v-model="model" :meta="meta" :is-page="true" infos="none" :on-configure="onEditorConfigure" />
|
||||
<ui-editor v-if="!loading" :config="renderer" v-model="model" :meta="meta" :is-page="true" infos="none" :on-configure="onEditorConfigure" :active-tab="2" />
|
||||
</ui-form>
|
||||
</template>
|
||||
|
||||
@@ -82,11 +82,6 @@
|
||||
|
||||
onLoad(form)
|
||||
{
|
||||
PagesApi.getRevisions(this.id).then(response =>
|
||||
{
|
||||
console.info(response);
|
||||
});
|
||||
|
||||
form.load(!this.id ? PagesApi.getEmpty(this.type, this.parent) : PagesApi.getById(this.id)).then(response =>
|
||||
{
|
||||
this.renderer = 'page.' + response.entity.pageTypeAlias;
|
||||
|
||||
@@ -2,8 +2,9 @@ import dayjs from 'dayjs';
|
||||
|
||||
const BYTE_UNIT = 'B';
|
||||
const UNITS = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const DATETIME_FORMAT = 'DD.MM.YY HH:mm';
|
||||
const DATE_FORMAT = 'DD.MM.YY';
|
||||
const TIME_FORMAT = 'HH:mm';
|
||||
const DATETIME_FORMAT = DATE_FORMAT + ' ' + TIME_FORMAT;
|
||||
|
||||
export default {
|
||||
/// <summary>
|
||||
@@ -59,6 +60,10 @@ export default {
|
||||
{
|
||||
format = DATE_FORMAT;
|
||||
}
|
||||
else if (format === 'time')
|
||||
{
|
||||
format = TIME_FORMAT;
|
||||
}
|
||||
|
||||
return dayjs(value).format(format);
|
||||
},
|
||||
|
||||
@@ -99,6 +99,12 @@ button::-moz-focus-inner
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
&.type-small
|
||||
{
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
&.type-light
|
||||
{
|
||||
background: var(--color-bg-bright-two);
|
||||
|
||||
@@ -9,19 +9,11 @@
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--color-shadow-short);
|
||||
/*max-width: 1104px;*/
|
||||
|
||||
h2
|
||||
|
||||
h2, h3
|
||||
{
|
||||
margin: -5px -32px var(--padding);
|
||||
padding: 0 var(--padding) 25px;
|
||||
//border-bottom: 1px solid var(--color-line-light);
|
||||
}
|
||||
|
||||
h3
|
||||
{
|
||||
margin: -5px -32px var(--padding);
|
||||
padding: 0 var(--padding) 25px;
|
||||
//border-bottom: 1px solid var(--color-line-light);
|
||||
margin: -5px 0 25px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ui-property-content &
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
.ui-link
|
||||
{
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline dotted;
|
||||
text-decoration: underline dotted var(--color-fg-dim-two);
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
@@ -8,10 +8,12 @@ namespace zero.Web.Controllers
|
||||
public class PagesController<T> : BackofficeController where T : IPage, new()
|
||||
{
|
||||
IPagesApi<T> Api;
|
||||
IRevisionsApi RevisionsApi;
|
||||
|
||||
public PagesController(IPagesApi<T> api)
|
||||
public PagesController(IPagesApi<T> api, IRevisionsApi revisionsApi)
|
||||
{
|
||||
Api = api;
|
||||
RevisionsApi = revisionsApi;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +30,7 @@ namespace zero.Web.Controllers
|
||||
ParentId = parent
|
||||
});
|
||||
|
||||
public async Task<IActionResult> GetRevisions([FromQuery] string id) => Json(await Api.GetRevisions(id));
|
||||
public async Task<IActionResult> GetRevisions([FromQuery] string id) => Json(await RevisionsApi.GetPaged<T>(id));
|
||||
|
||||
public async Task<IActionResult> Save([FromBody] T model) => Json(await Api.Save(model));
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace zero.Web.Defaults
|
||||
services.AddTransient<IToken, Token>();
|
||||
services.AddTransient<ISpacesApi, SpacesApi>();
|
||||
services.AddTransient<IPermissionsApi, PermissionsApi>();
|
||||
services.AddTransient<IRevisionsApi, RevisionsApi>();
|
||||
services.AddTransient<IMediaApi, MediaApi>();
|
||||
services.AddTransient<IMediaFolderApi, MediaFolderApi>();
|
||||
services.AddTransient<IMediaUploadApi, MediaUploadApi>();
|
||||
|
||||
Reference in New Issue
Block a user