mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
118 changed files with 2589 additions and 1160 deletions
@ -0,0 +1,120 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.OData.UriParser; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents |
||||
|
{ |
||||
|
internal class MongoContentCollection : MongoRepositoryBase<MongoContentEntity> |
||||
|
{ |
||||
|
private readonly string collectionName; |
||||
|
|
||||
|
public MongoContentCollection(IMongoDatabase database, string collectionName) |
||||
|
: base(database) |
||||
|
{ |
||||
|
this.collectionName = collectionName; |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); |
||||
|
} |
||||
|
|
||||
|
protected override string CollectionName() |
||||
|
{ |
||||
|
return collectionName; |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, ODataUriParser odataQuery, Status[] status = null, bool useDraft = false) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var propertyCalculator = FindExtensions.CreatePropertyCalculator(schema.SchemaDef, useDraft); |
||||
|
|
||||
|
var filter = FindExtensions.BuildQuery(odataQuery, schema.Id, status, propertyCalculator); |
||||
|
|
||||
|
var contentCount = Collection.Find(filter).CountAsync(); |
||||
|
var contentItems = |
||||
|
Collection.Find(filter) |
||||
|
.ContentTake(odataQuery) |
||||
|
.ContentSkip(odataQuery) |
||||
|
.ContentSort(odataQuery, propertyCalculator) |
||||
|
.Not(x => x.DataText) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
await Task.WhenAll(contentItems, contentCount); |
||||
|
|
||||
|
foreach (var entity in contentItems.Result) |
||||
|
{ |
||||
|
entity.ParseData(schema.SchemaDef); |
||||
|
} |
||||
|
|
||||
|
return ResultList.Create<IContentEntity>(contentItems.Result, contentCount.Result); |
||||
|
} |
||||
|
catch (NotSupportedException) |
||||
|
{ |
||||
|
throw new ValidationException("This odata operation is not supported."); |
||||
|
} |
||||
|
catch (NotImplementedException) |
||||
|
{ |
||||
|
throw new ValidationException("This odata operation is not supported."); |
||||
|
} |
||||
|
catch (MongoQueryException ex) |
||||
|
{ |
||||
|
if (ex.Message.Contains("17406")) |
||||
|
{ |
||||
|
throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<Guid> ids, Status[] status = null) |
||||
|
{ |
||||
|
var find = |
||||
|
status != null && status.Length > 0 ? |
||||
|
Collection.Find(x => x.IndexedSchemaId == schema.Id && ids.Contains(x.Id) && x.IsDeleted != true && status.Contains(x.Status)) : |
||||
|
Collection.Find(x => x.IndexedSchemaId == schema.Id && ids.Contains(x.Id)); |
||||
|
|
||||
|
var contentItems = find.Not(x => x.DataText).ToListAsync(); |
||||
|
var contentCount = find.CountAsync(); |
||||
|
|
||||
|
await Task.WhenAll(contentItems, contentCount); |
||||
|
|
||||
|
foreach (var entity in contentItems.Result) |
||||
|
{ |
||||
|
entity.ParseData(schema.SchemaDef); |
||||
|
} |
||||
|
|
||||
|
return ResultList.Create<IContentEntity>(contentItems.Result, contentCount.Result); |
||||
|
} |
||||
|
|
||||
|
public Task CleanupAsync(Guid id) |
||||
|
{ |
||||
|
return Collection.UpdateManyAsync( |
||||
|
Filter.And( |
||||
|
Filter.AnyEq(x => x.ReferencedIds, id), |
||||
|
Filter.AnyNe(x => x.ReferencedIdsDeleted, id)), |
||||
|
Update.AddToSet(x => x.ReferencedIdsDeleted, id)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,128 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.State; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents |
||||
|
{ |
||||
|
internal sealed class MongoContentDraftCollection : MongoContentCollection |
||||
|
{ |
||||
|
public MongoContentDraftCollection(IMongoDatabase database) |
||||
|
: base(database, "State_Content_Draft") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync( |
||||
|
Index |
||||
|
.Ascending(x => x.IndexedSchemaId) |
||||
|
.Ascending(x => x.Id) |
||||
|
.Ascending(x => x.IsDeleted)); |
||||
|
|
||||
|
await collection.Indexes.CreateOneAsync( |
||||
|
Index |
||||
|
.Text(x => x.DataText) |
||||
|
.Ascending(x => x.IndexedSchemaId) |
||||
|
.Ascending(x => x.IsDeleted) |
||||
|
.Ascending(x => x.Status)); |
||||
|
|
||||
|
await base.SetupCollectionAsync(collection); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids) |
||||
|
{ |
||||
|
var contentEntities = |
||||
|
await Collection.Find(x => x.IndexedSchemaId == schemaId && ids.Contains(x.Id) && x.IsDeleted != true).Only(x => x.Id) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
return ids.Except(contentEntities.Select(x => Guid.Parse(x["_id"].AsString))).ToList(); |
||||
|
} |
||||
|
|
||||
|
public Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback) |
||||
|
{ |
||||
|
return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) |
||||
|
.Not(x => x.DataByIds) |
||||
|
.Not(x => x.DataDraftByIds) |
||||
|
.Not(x => x.DataText) |
||||
|
.ForEachAsync(c => |
||||
|
{ |
||||
|
callback(c); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) |
||||
|
{ |
||||
|
var contentEntity = |
||||
|
await Collection.Find(x => x.IndexedSchemaId == schema.Id && x.Id == id && x.IsDeleted != true).Not(x => x.DataText) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
contentEntity?.ParseData(schema.SchemaDef); |
||||
|
|
||||
|
return contentEntity; |
||||
|
} |
||||
|
|
||||
|
public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func<Guid, Guid, Task<ISchemaEntity>> getSchema) |
||||
|
{ |
||||
|
var contentEntity = |
||||
|
await Collection.Find(x => x.Id == key).Not(x => x.DataText) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (contentEntity != null) |
||||
|
{ |
||||
|
var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); |
||||
|
|
||||
|
contentEntity?.ParseData(schema.SchemaDef); |
||||
|
|
||||
|
return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); |
||||
|
} |
||||
|
|
||||
|
return (null, EtagVersion.NotFound); |
||||
|
} |
||||
|
|
||||
|
public async Task UpsertAsync(MongoContentEntity content, long oldVersion) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
content.DataText = content.DataDraftByIds.ToFullText(); |
||||
|
|
||||
|
await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert); |
||||
|
} |
||||
|
catch (MongoWriteException ex) |
||||
|
{ |
||||
|
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) |
||||
|
{ |
||||
|
var existingVersion = |
||||
|
await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (existingVersion != null) |
||||
|
{ |
||||
|
throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents |
||||
|
{ |
||||
|
internal sealed class MongoContentPublishedCollection : MongoContentCollection |
||||
|
{ |
||||
|
public MongoContentPublishedCollection(IMongoDatabase database) |
||||
|
: base(database, "State_Content_Published") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection) |
||||
|
{ |
||||
|
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText).Ascending(x => x.IndexedSchemaId)); |
||||
|
|
||||
|
await collection.Indexes.CreateOneAsync( |
||||
|
Index |
||||
|
.Ascending(x => x.IndexedSchemaId) |
||||
|
.Ascending(x => x.Id)); |
||||
|
|
||||
|
await base.SetupCollectionAsync(collection); |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) |
||||
|
{ |
||||
|
var contentEntity = |
||||
|
await Collection.Find(x => x.IndexedSchemaId == schema.Id && x.Id == id).Not(x => x.DataText) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
contentEntity?.ParseData(schema.SchemaDef); |
||||
|
|
||||
|
return contentEntity; |
||||
|
} |
||||
|
|
||||
|
public Task UpsertAsync(MongoContentEntity content) |
||||
|
{ |
||||
|
content.DataText = content.DataByIds.ToFullText(); |
||||
|
content.DataDraftByIds = null; |
||||
|
content.ScheduleJob = null; |
||||
|
content.ScheduledAt = null; |
||||
|
|
||||
|
return Collection.ReplaceOneAsync(x => x.Id == content.Id, content, new UpdateOptions { IsUpsert = true }); |
||||
|
} |
||||
|
|
||||
|
public Task RemoveAsync(Guid id) |
||||
|
{ |
||||
|
return Collection.DeleteOneAsync(x => x.Id == id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
||||
|
{ |
||||
|
public sealed class DiscardChanges : ContentCommand |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class ScheduleJob |
||||
|
{ |
||||
|
public Guid Id { get; } |
||||
|
|
||||
|
public Status Status { get; } |
||||
|
|
||||
|
public RefToken ScheduledBy { get; } |
||||
|
|
||||
|
public Instant DueTime { get; } |
||||
|
|
||||
|
public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant dueTime) |
||||
|
{ |
||||
|
Id = id; |
||||
|
ScheduledBy = scheduledBy; |
||||
|
Status = status; |
||||
|
DueTime = dueTime; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Contents |
||||
|
{ |
||||
|
[EventType(nameof(ContentChangesDiscarded))] |
||||
|
public sealed class ContentChangesDiscarded : ContentEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Contents |
||||
|
{ |
||||
|
[EventType(nameof(ContentChangesPublished))] |
||||
|
public sealed class ContentChangesPublished : ContentEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Contents |
||||
|
{ |
||||
|
[EventType(nameof(ContentSchedulingCancelled))] |
||||
|
public sealed class ContentSchedulingCancelled : ContentEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Contents |
||||
|
{ |
||||
|
[EventType(nameof(ContentUpdateProposed))] |
||||
|
public sealed class ContentUpdateProposed : ContentEvent |
||||
|
{ |
||||
|
public NamedContentData Data { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Threading; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Caching |
||||
|
{ |
||||
|
public sealed class AsyncLocalCache : ILocalCache |
||||
|
{ |
||||
|
private static readonly AsyncLocal<ConcurrentDictionary<object, object>> Cache = new AsyncLocal<ConcurrentDictionary<object, object>>(); |
||||
|
private static readonly AsyncLocalCleaner Cleaner; |
||||
|
|
||||
|
private sealed class AsyncLocalCleaner : IDisposable |
||||
|
{ |
||||
|
private readonly AsyncLocal<ConcurrentDictionary<object, object>> cache; |
||||
|
|
||||
|
public AsyncLocalCleaner(AsyncLocal<ConcurrentDictionary<object, object>> cache) |
||||
|
{ |
||||
|
this.cache = cache; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
cache.Value = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static AsyncLocalCache() |
||||
|
{ |
||||
|
Cleaner = new AsyncLocalCleaner(Cache); |
||||
|
} |
||||
|
|
||||
|
public IDisposable StartContext() |
||||
|
{ |
||||
|
Cache.Value = new ConcurrentDictionary<object, object>(); |
||||
|
|
||||
|
return Cleaner; |
||||
|
} |
||||
|
|
||||
|
public void Add(object key, object value) |
||||
|
{ |
||||
|
var cacheKey = GetCacheKey(key); |
||||
|
|
||||
|
var cache = Cache.Value; |
||||
|
|
||||
|
if (cache != null) |
||||
|
{ |
||||
|
cache[cacheKey] = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Remove(object key) |
||||
|
{ |
||||
|
var cacheKey = GetCacheKey(key); |
||||
|
|
||||
|
var cache = Cache.Value; |
||||
|
|
||||
|
if (cache != null) |
||||
|
{ |
||||
|
cache.TryRemove(cacheKey, out var value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool TryGetValue(object key, out object value) |
||||
|
{ |
||||
|
var cacheKey = GetCacheKey(key); |
||||
|
|
||||
|
var cache = Cache.Value; |
||||
|
|
||||
|
if (cache != null) |
||||
|
{ |
||||
|
return cache.TryGetValue(cacheKey, out value); |
||||
|
} |
||||
|
|
||||
|
value = null; |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static string GetCacheKey(object key) |
||||
|
{ |
||||
|
return $"CACHE_{key}"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,68 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Microsoft.AspNetCore.Http; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Caching |
|
||||
{ |
|
||||
public sealed class HttpRequestCache : IRequestCache |
|
||||
{ |
|
||||
private readonly IHttpContextAccessor httpContextAccessor; |
|
||||
|
|
||||
public HttpRequestCache(IHttpContextAccessor httpContextAccessor) |
|
||||
{ |
|
||||
Guard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); |
|
||||
|
|
||||
this.httpContextAccessor = httpContextAccessor; |
|
||||
} |
|
||||
|
|
||||
public void Add(object key, object value) |
|
||||
{ |
|
||||
var cacheKey = GetCacheKey(key); |
|
||||
|
|
||||
var items = httpContextAccessor.HttpContext?.Items; |
|
||||
|
|
||||
if (items != null) |
|
||||
{ |
|
||||
items[cacheKey] = value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Remove(object key) |
|
||||
{ |
|
||||
var cacheKey = GetCacheKey(key); |
|
||||
|
|
||||
var items = httpContextAccessor.HttpContext?.Items; |
|
||||
|
|
||||
if (items != null) |
|
||||
{ |
|
||||
items?.Remove(cacheKey); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public bool TryGetValue(object key, out object value) |
|
||||
{ |
|
||||
var cacheKey = GetCacheKey(key); |
|
||||
|
|
||||
var items = httpContextAccessor.HttpContext?.Items; |
|
||||
|
|
||||
if (items != null) |
|
||||
{ |
|
||||
return items.TryGetValue(cacheKey, out value); |
|
||||
} |
|
||||
|
|
||||
value = null; |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
private static string GetCacheKey(object key) |
|
||||
{ |
|
||||
return $"CACHE_{key}"; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Commands |
||||
|
{ |
||||
|
public sealed class ReadonlyCommandMiddleware : ICommandMiddleware |
||||
|
{ |
||||
|
private readonly IOptions<ReadonlyOptions> options; |
||||
|
|
||||
|
public ReadonlyCommandMiddleware(IOptions<ReadonlyOptions> options) |
||||
|
{ |
||||
|
Guard.NotNull(options, nameof(options)); |
||||
|
|
||||
|
this.options = options; |
||||
|
} |
||||
|
|
||||
|
public Task HandleAsync(CommandContext context, Func<Task> next) |
||||
|
{ |
||||
|
if (options.Value.IsReadonly) |
||||
|
{ |
||||
|
throw new DomainException("Application is in readonly mode at the moment."); |
||||
|
} |
||||
|
|
||||
|
return next(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Commands |
||||
|
{ |
||||
|
public sealed class ReadonlyOptions |
||||
|
{ |
||||
|
public bool IsReadonly { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Orleans; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public sealed class LocalCacheFilter : IIncomingGrainCallFilter |
||||
|
{ |
||||
|
private readonly ILocalCache localCache; |
||||
|
|
||||
|
public LocalCacheFilter(ILocalCache localCache) |
||||
|
{ |
||||
|
Guard.NotNull(localCache, nameof(localCache)); |
||||
|
|
||||
|
this.localCache = localCache; |
||||
|
} |
||||
|
|
||||
|
public async Task Invoke(IIncomingGrainCallContext context) |
||||
|
{ |
||||
|
using (localCache.StartContext()) |
||||
|
{ |
||||
|
await context.Invoke(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Contents.Models |
||||
|
{ |
||||
|
public sealed class ScheduleJobDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The id of the schedule job.
|
||||
|
/// </summary>
|
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The new status.
|
||||
|
/// </summary>
|
||||
|
public Status Status { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The user who schedule the content.
|
||||
|
/// </summary>
|
||||
|
public RefToken ScheduledBy { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The target date and time when the content should be scheduled.
|
||||
|
/// </summary>
|
||||
|
public Instant DueTime { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
|
||||
|
namespace Squidex.Pipeline |
||||
|
{ |
||||
|
public sealed class LocalCacheMiddleware : IMiddleware |
||||
|
{ |
||||
|
private readonly ILocalCache localCache; |
||||
|
|
||||
|
public LocalCacheMiddleware(ILocalCache localCache) |
||||
|
{ |
||||
|
Guard.NotNull(localCache, nameof(localCache)); |
||||
|
|
||||
|
this.localCache = localCache; |
||||
|
} |
||||
|
|
||||
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next) |
||||
|
{ |
||||
|
using (localCache.StartContext()) |
||||
|
{ |
||||
|
await next(context); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
<span *ngIf="!scheduledTo"> |
||||
|
<span class="content-status content-status-{{displayStatus | lowercase}}" #statusIcon> |
||||
|
<i class="icon-circle"></i> |
||||
|
</span> |
||||
|
|
||||
|
<sqx-tooltip [target]="statusIcon">{{displayStatus}}</sqx-tooltip> |
||||
|
</span> |
||||
|
|
||||
|
<span *ngIf="scheduledTo"> |
||||
|
<span class="content-status content-status-{{scheduledTo | lowercase}}" #statusIcon> |
||||
|
<i class="icon-clock"></i> |
||||
|
</span> |
||||
|
|
||||
|
<sqx-tooltip position="topRight" [target]="statusIcon">Will be set to '{{scheduledTo}}' at {{scheduledAt | sqxFullDateTime}}</sqx-tooltip> |
||||
|
</span> |
||||
|
|
||||
|
<span class="content-status-label" *ngIf="showLabel">{{displayStatus}}</span> |
||||
@ -0,0 +1,38 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.content-status { |
||||
|
& { |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
&-published { |
||||
|
color: $color-theme-green; |
||||
|
} |
||||
|
|
||||
|
&-draft { |
||||
|
color: $color-text-decent; |
||||
|
} |
||||
|
|
||||
|
&-archived { |
||||
|
color: $color-theme-error; |
||||
|
} |
||||
|
|
||||
|
&-pending { |
||||
|
color: $color-dark-black; |
||||
|
} |
||||
|
|
||||
|
&-label { |
||||
|
color: $color-text; |
||||
|
} |
||||
|
|
||||
|
&-tooltip { |
||||
|
@include border-radius; |
||||
|
background: $color-tooltip; |
||||
|
border: 0; |
||||
|
font-size: .9rem; |
||||
|
font-weight: normal; |
||||
|
color: $color-dark-foreground; |
||||
|
padding: .75rem; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; |
||||
|
|
||||
|
import { DateTime } from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-content-status', |
||||
|
styleUrls: ['./content-status.component.scss'], |
||||
|
templateUrl: './content-status.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class ContentStatusComponent { |
||||
|
@Input() |
||||
|
public status: string; |
||||
|
|
||||
|
@Input() |
||||
|
public scheduledTo?: string; |
||||
|
|
||||
|
@Input() |
||||
|
public scheduledAt?: DateTime; |
||||
|
|
||||
|
@Input() |
||||
|
public isPending: any; |
||||
|
|
||||
|
@Input() |
||||
|
public showLabel = false; |
||||
|
|
||||
|
public get displayStatus() { |
||||
|
return !!this.isPending ? 'Pending' : this.status; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,30 @@ |
|||||
|
<ng-container *sqxModalView="dueTimeDialog;onRoot:true"> |
||||
|
<sqx-modal-dialog (closed)="cancelStatusChange()"> |
||||
|
<ng-container title> |
||||
|
{{dueTimeAction}} content item(s) |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container content> |
||||
|
<div class="form-check"> |
||||
|
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately"> |
||||
|
<label class="form-check-label" for="immediately"> |
||||
|
{{dueTimeAction}} content item(s) immediately. |
||||
|
</label> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-check"> |
||||
|
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled"> |
||||
|
<label class="form-check-label" for="scheduled"> |
||||
|
{{dueTimeAction}} content item(s) at a later point date and time. |
||||
|
</label> |
||||
|
</div> |
||||
|
|
||||
|
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container footer> |
||||
|
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button> |
||||
|
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit>Confirm</button> |
||||
|
</ng-container> |
||||
|
</sqx-modal-dialog> |
||||
|
</ng-container> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
@ -0,0 +1,52 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
import { Observable, Subject } from 'rxjs'; |
||||
|
|
||||
|
import { fadeAnimation, ModalView } from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-due-time-selector', |
||||
|
styleUrls: ['./due-time-selector.component.scss'], |
||||
|
templateUrl: './due-time-selector.component.html', |
||||
|
animations: [ |
||||
|
fadeAnimation |
||||
|
] |
||||
|
}) |
||||
|
export class DueTimeSelectorComponent { |
||||
|
public dueTimeDialog = new ModalView(); |
||||
|
public dueTime: string | null = ''; |
||||
|
public dueTimeFunction: Subject<string | null>; |
||||
|
public dueTimeAction: string | null = ''; |
||||
|
public dueTimeMode = 'Immediately'; |
||||
|
|
||||
|
public selectDueTime(action: string): Observable<string | null> { |
||||
|
this.dueTimeAction = action; |
||||
|
this.dueTimeFunction = new Subject<string | null>(); |
||||
|
this.dueTimeDialog.show(); |
||||
|
|
||||
|
return this.dueTimeFunction; |
||||
|
} |
||||
|
|
||||
|
public confirmStatusChange() { |
||||
|
const result = this.dueTimeMode === 'Immediately' ? null : this.dueTime; |
||||
|
|
||||
|
this.dueTimeFunction.next(result); |
||||
|
this.dueTimeFunction.complete(); |
||||
|
|
||||
|
this.cancelStatusChange(); |
||||
|
} |
||||
|
|
||||
|
public cancelStatusChange() { |
||||
|
this.dueTimeMode = 'Immediately'; |
||||
|
this.dueTimeDialog.hide(); |
||||
|
this.dueTimeFunction = null!; |
||||
|
this.dueTime = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -1,3 +1,3 @@ |
|||||
<div class="tooltip-container" *sqxModalView="modal;onRoot:true;closeAuto:false" [sqxModalTarget]="target" position="topLeft"> |
<div class="tooltip-container" *sqxModalView="modal;onRoot:true;closeAuto:false" [sqxModalTarget]="target" [position]="position"> |
||||
<ng-content></ng-content> |
<ng-content></ng-content> |
||||
</div> |
</div> |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue