diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 8b3167968..8eed38bd8 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/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))); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index 66feb04e3..c45e1bd03 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/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: diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index 687b712d0..fac3ca651 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/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: diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs index efd04181e..1d18f7f60 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs +++ b/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 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); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index 21d6c7450..615d438de 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/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: diff --git a/src/Squidex.Domain.Apps.Entities/Query.cs b/src/Squidex.Domain.Apps.Entities/Query.cs index f90d78d84..a6afff384 100644 --- a/src/Squidex.Domain.Apps.Entities/Query.cs +++ b/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 => { diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index c003d0f94..879282c22 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/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: diff --git a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs index 0fb7c281e..3387b428d 100644 --- a/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs +++ b/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 { diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index fd5703d53..4633f0c14 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/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 assetsConfig) + IOptions assetsConfig, + ITagService tagService) : base(commandBus) { this.assetsConfig = assetsConfig.Value; this.assetQuery = assetQuery; this.assetStatsRepository = assetStatsRepository; this.appPlanProvider = appPlanProvider; + this.tagService = tagService; + } + + /// + /// Get assets tags. + /// + /// The name of the app. + /// + /// 200 => Assets returned. + /// 404 => App not found. + /// + /// + /// Get all tags for assets. + /// + [MustBeAppReader] + [HttpGet] + [Route("apps/{app}/assets/tags")] + [ProducesResponseType(typeof(Dictionary), 200)] + [ApiCosts(1)] + public async Task GetTags(string app) + { + var response = await tagService.GetTagsAsync(App.Id, TagGroups.Assets); + + return Ok(response); } /// diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index e79f2a915..973b592c5 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -63,6 +63,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.html b/src/Squidex/app/features/assets/pages/assets-page.component.html index cdde0ccfa..994493452 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.html +++ b/src/Squidex/app/features/assets/pages/assets-page.component.html @@ -1,6 +1,6 @@ - + Assets @@ -21,4 +21,29 @@ + + + + + + diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.scss b/src/Squidex/app/features/assets/pages/assets-page.component.scss index fbb752506..550096d63 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.scss +++ b/src/Squidex/app/features/assets/pages/assets-page.component.scss @@ -1,2 +1,26 @@ @import '_vars'; -@import '_mixins'; \ No newline at end of file +@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; +} \ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.ts b/src/Squidex/app/features/assets/pages/assets-page.component.ts index 18173f82b..0f0ccfd8b 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.ts +++ b/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(); } diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index 7092cfcf7..9eaa13625 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/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!)); diff --git a/src/Squidex/app/framework/angular/panel.component.html b/src/Squidex/app/framework/angular/panel.component.html index bdad29894..26729563d 100644 --- a/src/Squidex/app/framework/angular/panel.component.html +++ b/src/Squidex/app/framework/angular/panel.component.html @@ -30,7 +30,7 @@ -
+
diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index f0317de6a..7c70ef8fc 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/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; diff --git a/src/Squidex/app/shared/components/asset.component.html b/src/Squidex/app/shared/components/asset.component.html index 8b26c3483..2d91158f4 100644 --- a/src/Squidex/app/shared/components/asset.component.html +++ b/src/Squidex/app/shared/components/asset.component.html @@ -1,6 +1,6 @@
-
+
{{asset.fileType}} diff --git a/src/Squidex/app/shared/components/asset.component.scss b/src/Squidex/app/shared/components/asset.component.scss index 5beba1cc8..217185eb6 100644 --- a/src/Squidex/app/shared/components/asset.component.scss +++ b/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 { diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index 258da493b..c3643b550 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/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'); diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 7219228d4..c7aa95731 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/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 { + 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 => response)); + } + + public getAssets(appName: string, take: number, skip: number, query?: string, tag?: string, ids?: string[]): Observable { 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(this.http, url).pipe( diff --git a/src/Squidex/app/shared/state/assets.state.ts b/src/Squidex/app/shared/state/assets.state.ts index f931f944f..0fe1eafbe 100644 --- a/src/Squidex/app/shared/state/assets.state.ts +++ b/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; assetsPager: Pager; assetsQuery?: string; @@ -30,6 +33,14 @@ interface Snapshot { @Injectable() export class AssetsState extends State { + 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 { 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 { @@ -59,17 +70,20 @@ export class AssetsState extends State { } private loadInternal(isReload = false): Observable { - 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 { public delete(asset: AssetDto): Observable { 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 { }); } + public selectTag(tag: string): Observable { + this.next(s => ({ ...s, assetsPager: new Pager(0, 0, 30), tag })); + + return this.loadInternal(); + } + public search(query: string): Observable { this.next(s => ({ ...s, assetsPager: new Pager(0, 0, 30), assetsQuery: query })); diff --git a/src/Squidex/app/theme/_panels.scss b/src/Squidex/app/theme/_panels.scss index 3918a476b..6124ea873 100644 --- a/src/Squidex/app/theme/_panels.scss +++ b/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);