Browse Source

Grain service.

pull/308/head
Sebastian 8 years ago
parent
commit
a5e31583a3
  1. 1
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  3. 4
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  4. 2
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  5. 2
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  6. 2
      src/Squidex.Domain.Apps.Entities/Query.cs
  7. 2
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  8. 5
      src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs
  9. 29
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  10. 3
      src/Squidex/Config/Domain/EntitiesServices.cs
  11. 27
      src/Squidex/app/features/assets/pages/assets-page.component.html
  12. 26
      src/Squidex/app/features/assets/pages/assets-page.component.scss
  13. 4
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  14. 2
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  15. 2
      src/Squidex/app/framework/angular/panel.component.html
  16. 3
      src/Squidex/app/framework/angular/panel.component.ts
  17. 2
      src/Squidex/app/shared/components/asset.component.html
  18. 4
      src/Squidex/app/shared/components/asset.component.scss
  19. 40
      src/Squidex/app/shared/services/assets.service.spec.ts
  20. 22
      src/Squidex/app/shared/services/assets.service.ts
  21. 34
      src/Squidex/app/shared/state/assets.state.ts
  22. 5
      src/Squidex/app/theme/_panels.scss

1
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -46,6 +46,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.Ascending(x => x.AppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.FileName)
.Ascending(x => x.Tags)
.Descending(x => x.LastModified)));
}

2
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
AssignContributor(c);
return EntityCreatedResult.Create(c.ContributorId, (long)Version);
return EntityCreatedResult.Create(c.ContributorId, Version);
});
case RemoveContributor removeContributor:

4
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -67,10 +67,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
Rename(c);
});
case DeleteAsset deleteAsset:
return UpdateAsync(deleteAsset, c =>
return UpdateAsync(deleteAsset, async c =>
{
GuardAsset.CanDelete(c);
await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags);
Delete(c);
});
case TagAsset tagAsset:

2
src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs

@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets)
{
var tags = assets.SelectMany(x => x.Tags).Distinct().ToArray();
var tags = assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct().ToArray();
var tagsById = await tagService.DenormalizeTagsAsync(appId, TagGroups.Assets, tags);

2
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Create(c);
return EntityCreatedResult.Create(c.Data, (long)Version);
return EntityCreatedResult.Create(c.Data, Version);
});
case UpdateContent updateContent:

2
src/Squidex.Domain.Apps.Entities/Query.cs

@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities
public Query WithIds(string ids)
{
if (string.IsNullOrEmpty(ids))
if (!string.IsNullOrEmpty(ids))
{
return Clone(c =>
{

2
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
id = ((IArrayField)Snapshot.SchemaDef.FieldsById[c.ParentFieldId.Value]).FieldsByName[c.Name].Id;
}
return EntityCreatedResult.Create(id, (long)Version);
return EntityCreatedResult.Create(id, Version);
});
case CreateSchema createSchema:

5
src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs

@ -69,6 +69,11 @@ namespace Squidex.Domain.Apps.Entities.Tags
if (found.Value != null)
{
tagId = found.Key;
if (ids == null || !ids.Contains(tagId))
{
found.Value.Count++;
}
}
else
{

29
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
@ -38,6 +39,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
private readonly IAssetQueryService assetQuery;
private readonly IAssetStatsRepository assetStatsRepository;
private readonly IAppPlansProvider appPlanProvider;
private readonly ITagService tagService;
private readonly AssetConfig assetsConfig;
public AssetsController(
@ -45,13 +47,38 @@ namespace Squidex.Areas.Api.Controllers.Assets
IAssetQueryService assetQuery,
IAssetStatsRepository assetStatsRepository,
IAppPlansProvider appPlanProvider,
IOptions<AssetConfig> assetsConfig)
IOptions<AssetConfig> assetsConfig,
ITagService tagService)
: base(commandBus)
{
this.assetsConfig = assetsConfig.Value;
this.assetQuery = assetQuery;
this.assetStatsRepository = assetStatsRepository;
this.appPlanProvider = appPlanProvider;
this.tagService = tagService;
}
/// <summary>
/// Get assets tags.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Assets returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// Get all tags for assets.
/// </remarks>
[MustBeAppReader]
[HttpGet]
[Route("apps/{app}/assets/tags")]
[ProducesResponseType(typeof(Dictionary<string, int>), 200)]
[ApiCosts(1)]
public async Task<IActionResult> GetTags(string app)
{
var response = await tagService.GetTagsAsync(App.Id, TagGroups.Assets);
return Ok(response);
}
/// <summary>

3
src/Squidex/Config/Domain/EntitiesServices.cs

@ -63,6 +63,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AppProvider>()
.As<IAppProvider>();
services.AddSingletonAs<AssetQueryService>()
.As<IAssetQueryService>();
services.AddSingletonAs<ContentQueryService>()
.As<IContentQueryService>();

27
src/Squidex/app/features/assets/pages/assets-page.component.html

@ -1,6 +1,6 @@
<sqx-title message="{app} | Assets" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="*">
<sqx-panel desiredWidth="*" showSidebar="true" sidebarClass="wide">
<ng-container title>
Assets
</ng-container>
@ -21,4 +21,29 @@
<ng-container content>
<sqx-assets-list [state]="assetsState"></sqx-assets-list>
</ng-container>
<ng-container sidebar>
<div class="section">
<a class="row tag" (click)="selectTag(undefined)" [class.active]="!assetsState.snapshot.tag">
<div class="col">
All Assets
</div>
</a>
</div>
<div class="section">
<h3>Tags</h3>
<ng-container *ngIf="assetsState.tags | async; let tags">
<a class="row tag" *ngFor="let tag of tags | sqxKeys" (click)="selectTag(tag)" [class.active]="tag === assetsState.snapshot.tag">
<div class="col">
{{tag}}
</div>
<div class="col col-auto">
{{tags[tag]}}
</div>
</a>
</ng-container>
</div>
</ng-container>
</sqx-panel>

26
src/Squidex/app/features/assets/pages/assets-page.component.scss

@ -1,2 +1,26 @@
@import '_vars';
@import '_mixins';
@import '_mixins';
.section {
border-top: 1px solid $color-border;
padding: 1rem;
}
.tag {
& {
padding: .25rem 0;
}
&.active {
font-weight: bold;
}
&.active,
&:hover {
background: $color-background;
}
}
a.tag {
cursor: pointer !important;
}

4
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -39,6 +39,10 @@ export class AssetsPageComponent implements OnInit {
this.assetsState.search(this.assetsFilter.value).pipe(onErrorResumeNext()).subscribe();
}
public selectTag(tag: string) {
this.assetsState.selectTag(tag).pipe(onErrorResumeNext()).subscribe();
}
public goNext() {
this.assetsState.goNext().pipe(onErrorResumeNext()).subscribe();
}

2
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -53,7 +53,7 @@ export class AssetsEditorComponent implements ControlValueAccessor {
if (!Types.isEquals(obj, this.oldAssets.map(x => x.id).values)) {
const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, obj)
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj)
.subscribe(dtos => {
this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!));

2
src/Squidex/app/framework/angular/panel.component.html

@ -30,7 +30,7 @@
<ng-content select=[content]></ng-content>
</div>
<div class="panel-sidebar" *ngIf="showSidebar">
<div class="panel-sidebar {{sidebarClass}}" *ngIf="showSidebar">
<ng-content select=[sidebar]></ng-content>
</div>
</div>

3
src/Squidex/app/framework/angular/panel.component.ts

@ -54,6 +54,9 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
@Input()
public contentClass = '';
@Input()
public sidebarClass = '';
@ViewChild('panel')
public panel: ElementRef;

2
src/Squidex/app/shared/components/asset.component.html

@ -1,6 +1,6 @@
<div class="card" [class.selectable]="isSelectable" [class.border-primary]="isSelected" (click)="selected.emit(asset)" (sqxFileDrop)="updateFile($event)">
<div class="card-body">
<div class="file-preview" *ngIf="asset && progress == 0" @fade>
<div class="file-preview" *ngIf="asset && progress === 0" @fade>
<span class="file-type" *ngIf="asset.fileType">
{{asset.fileType}}
</span>

4
src/Squidex/app/shared/components/asset.component.scss

@ -73,6 +73,7 @@
.card {
& {
@include overlay-container;
min-height: $asset-height + 1.5rem;
}
&.selectable {
@ -81,7 +82,8 @@
&-body {
position: relative;
height: 0.7 * $asset-height;
min-height: 0.75 * $asset-height;
max-height: 0.75 * $asset-height;
}
&-footer {

40
src/Squidex/app/shared/services/assets.service.spec.ts

@ -90,6 +90,31 @@ describe('AssetsService', () => {
httpMock.verify();
}));
it('should make get request to get asset tags',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let tags: any;
assetsService.getTags('my-app').subscribe(result => {
tags = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/tags');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
tag1: 1,
tag2: 4
});
expect(tags!).toEqual({
tag1: 1,
tag2: 4
});
}));
it('should make get request to get assets',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
@ -244,10 +269,23 @@ describe('AssetsService', () => {
req.flush({ total: 10, items: [] });
}));
it('should append query to find by name and tag',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
assetsService.getAssets('my-app', 17, 13, 'my-query', 'tag1').subscribe();
const req = httpMock.expectOne(`http://service/p/api/apps/my-app/assets?$filter=contains(fileName,'my-query') and tag eq 'tag1'&$top=17&$skip=13`);
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ total: 10, items: [] });
}));
it('should append ids query to find by ids',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
assetsService.getAssets('my-app', 0, 0, undefined, ['12', '23']).subscribe();
assetsService.getAssets('my-app', 0, 0, undefined, undefined, ['12', '23']).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?ids=12,23');

22
src/Squidex/app/shared/services/assets.service.ts

@ -124,7 +124,14 @@ export class AssetsService {
) {
}
public getAssets(appName: string, take: number, skip: number, query?: string, ids?: string[]): Observable<AssetsDto> {
public getTags(appName: string): Observable<{ [name: string]: number }> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/tags`);
return this.http.get(url).pipe(
map(response => <any>response));
}
public getAssets(appName: string, take: number, skip: number, query?: string, tag?: string, ids?: string[]): Observable<AssetsDto> {
let fullQuery = '';
if (ids) {
@ -132,8 +139,18 @@ export class AssetsService {
} else {
const queries: string[] = [];
const filters: string[] = [];
if (query && query.length > 0) {
queries.push(`$filter=contains(fileName,'${encodeURIComponent(query)}')`);
filters.push(`contains(fileName,'${encodeURIComponent(query)}')`);
}
if (tag && tag.length > 0) {
filters.push(`tags eq '${encodeURIComponent(tag)}'`);
}
if (filters.length > 0) {
queries.push(`$filter=${filters.join(' and ')}`);
}
queries.push(`$top=${take}`);
@ -142,7 +159,6 @@ export class AssetsService {
fullQuery = queries.join('&');
}
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`);
return HTTP.getVersioned<any>(this.http, url).pipe(

34
src/Squidex/app/shared/state/assets.state.ts

@ -6,7 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import {
@ -21,6 +21,9 @@ import { AssetDto, AssetsService} from './../services/assets.service';
import { AppsState } from './apps.state';
interface Snapshot {
tags: { [name: string]: number };
tag?: string;
assets: ImmutableArray<AssetDto>;
assetsPager: Pager;
assetsQuery?: string;
@ -30,6 +33,14 @@ interface Snapshot {
@Injectable()
export class AssetsState extends State<Snapshot> {
public tags =
this.changes.pipe(map(x => x.tags),
distinctUntilChanged());
public tag =
this.changes.pipe(map(x => x.tag),
distinctUntilChanged());
public assets =
this.changes.pipe(map(x => x.assets),
distinctUntilChanged());
@ -47,7 +58,7 @@ export class AssetsState extends State<Snapshot> {
private readonly assetsService: AssetsService,
private readonly dialogs: DialogService
) {
super({ assets: ImmutableArray.empty(), assetsPager: new Pager(0, 0, 30) });
super({ assets: ImmutableArray.empty(), assetsPager: new Pager(0, 0, 30), tags: {} });
}
public load(isReload = false): Observable<any> {
@ -59,17 +70,20 @@ export class AssetsState extends State<Snapshot> {
}
private loadInternal(isReload = false): Observable<any> {
return this.assetsService.getAssets(this.appName, this.snapshot.assetsPager.pageSize, this.snapshot.assetsPager.skip, this.snapshot.assetsQuery).pipe(
return combineLatest(
this.assetsService.getAssets(this.appName, this.snapshot.assetsPager.pageSize, this.snapshot.assetsPager.skip, this.snapshot.assetsQuery, this.snapshot.tag),
this.assetsService.getTags(this.appName)
).pipe(
tap(dtos => {
if (isReload) {
this.dialogs.notifyInfo('Assets reloaded.');
}
this.next(s => {
const assets = ImmutableArray.of(dtos.items);
const assetsPager = s.assetsPager.setCount(dtos.total);
const assets = ImmutableArray.of(dtos[0].items);
const assetsPager = s.assetsPager.setCount(dtos[0].total);
return { ...s, assets, assetsPager, isLoaded: true };
return { ...s, assets, assetsPager, isLoaded: true, tags: dtos[1] };
});
}),
notify(this.dialogs));
@ -86,7 +100,7 @@ export class AssetsState extends State<Snapshot> {
public delete(asset: AssetDto): Observable<any> {
return this.assetsService.deleteAsset(this.appName, asset.id, asset.version).pipe(
tap(dto => {
tap(() => {
return this.next(s => {
const assets = s.assets.filter(x => x.id !== asset.id);
const assetsPager = s.assetsPager.decrementCount();
@ -105,6 +119,12 @@ export class AssetsState extends State<Snapshot> {
});
}
public selectTag(tag: string): Observable<any> {
this.next(s => ({ ...s, assetsPager: new Pager(0, 0, 30), tag }));
return this.loadInternal();
}
public search(query: string): Observable<any> {
this.next(s => ({ ...s, assetsPager: new Pager(0, 0, 30), assetsQuery: query }));

5
src/Squidex/app/theme/_panels.scss

@ -152,6 +152,11 @@
max-width: $panel-sidebar;
}
&.wide {
min-width: 16rem;
max-width: 16rem;
}
& .panel-link {
& {
@include transition(background-color .3s ease);

Loading…
Cancel
Save