소스 검색

Tracks paging and filtering

Lukas Angerer 3 년 전
부모
커밋
761e831325

+ 13 - 0
src/RunnersMeet.Client/src/app/pages/tracks-page/tracks-page.component.html

@@ -1,4 +1,17 @@
+<form (ngSubmit)="search($event)">
+	<div>
+		<input type="checkbox" name="showMyTracks" [(ngModel)]="showMyTracks" /> Show <em>my</em> tracks
+	</div>
+	<div>
+		Track name: <input type="text" name="nameFilter" [(ngModel)]="nameFilter" />
+	</div>
+	<div>
+		<button type="submit">Search</button>
+	</div>
+</form>
 Tracks:
 <ul>
 	<li *ngFor="let track of tracks | async"><app-track-list-item [track]="track"></app-track-list-item></li>
 </ul>
+
+<button type="button" (click)="loadMore()">More</button>

+ 34 - 2
src/RunnersMeet.Client/src/app/pages/tracks-page/tracks-page.component.ts

@@ -1,6 +1,8 @@
 import { Component } from '@angular/core';
 import { Track } from 'src/app/tracks/track';
 import { TracksApiService } from 'src/app/tracks-api.service';
+import { TrackSearchParams } from 'src/app/tracks/track-search-params';
+import { Subject } from 'rxjs';
 
 @Component({
 	selector: 'app-tracks-page',
@@ -8,11 +10,41 @@ import { TracksApiService } from 'src/app/tracks-api.service';
 	styleUrls: ['./tracks-page.component.scss']
 })
 export class TracksPageComponent {
-	public tracks: Promise<Track[]>;
+	private _tracks: Track[] = [];
+	public tracks: Subject<Track[]> = new Subject<Track[]>();
+
+	public showMyTracks: boolean = false;
+	public nameFilter: string = '';
+
+	private searchParams: TrackSearchParams = new TrackSearchParams();
 
 	public constructor(
 		private readonly tracksApi: TracksApiService
 	) {
-		this.tracks = this.tracksApi.getTracks();
+		this.updateTracks();
+	}
+
+	public search(event: SubmitEvent): void {
+		this.searchParams = new TrackSearchParams();
+		this.searchParams.owner = this.showMyTracks ? 'me' : undefined;
+		this.searchParams.filter = this.nameFilter ? this.nameFilter : undefined;
+		this.updateTracks()
+	}
+
+	public loadMore(): void {
+		this.searchParams.page++;
+		this.updateTracks();
+	}
+
+	private updateTracks(): void {
+		this.tracksApi.getTracks(this.searchParams)
+			.then(trackPage => {
+				if (this.searchParams.page === 0) {
+					this._tracks = trackPage;
+				} else {
+					this._tracks.push(...trackPage);
+				}
+				this.tracks.next(this._tracks)
+			});
 	}
 }

+ 4 - 3
src/RunnersMeet.Client/src/app/tracks-api.service.ts

@@ -1,8 +1,9 @@
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { lastValueFrom } from 'rxjs';
 import { ConfigService } from './config.service';
 import { Track } from './tracks/track';
+import { TrackSearchParams } from './tracks/track-search-params';
 
 @Injectable({
 	providedIn: 'root'
@@ -14,8 +15,8 @@ export class TracksApiService {
 		private readonly http: HttpClient
 	) { }
 
-	public getTracks(): Promise<Track[]> {
-		return lastValueFrom(this.http.get<Track[]>(this.config.apiUri(`/api/tracks`)));
+	public getTracks(searchParams: TrackSearchParams): Promise<Track[]> {
+		return lastValueFrom(this.http.get<Track[]>(this.config.apiUri(`/api/tracks`), { params: searchParams.toHttpParams() }));
 	}
 
 	public createTrack(formData: FormData): Promise<Track> {

+ 19 - 0
src/RunnersMeet.Client/src/app/tracks/track-search-params.ts

@@ -0,0 +1,19 @@
+import { HttpParams } from "@angular/common/http";
+
+export class TrackSearchParams {
+	public owner?: string;
+	public filter?: string;
+	public page: number = 0;
+
+	public toHttpParams(): HttpParams {
+		let result = new HttpParams();
+		if (this.owner) {
+			result = result.set('owner', this.owner);
+		}
+		if (this.filter) {
+			result = result.set('filter', this.filter);
+		}
+		result = result.set('page', this.page);
+		return result;
+	}
+}

+ 21 - 2
src/RunnersMeet.Server/Controllers/TracksController.cs

@@ -13,6 +13,8 @@ namespace RunnersMeet.Server.Controllers;
 [Authorize("Tracks")]
 public class TracksController : ControllerBase
 {
+	private const int PageSize = 3;
+
 	private readonly IFileStorage _fileStorage;
 	private readonly GpxParser _gpxParser;
 	private readonly QueryFactory _queryFactory;
@@ -27,9 +29,26 @@ public class TracksController : ControllerBase
 	}
 
 	[HttpGet]
-	public ActionResult<IEnumerable<Track>> GetTracks()
+	public ActionResult<IEnumerable<Track>> GetTracks([FromQuery] int? page, [FromQuery] string? owner, [FromQuery] string? filter)
 	{
-		return Ok(_queryFactory.TracksQuery().Get());
+		var query = _queryFactory.TracksQuery();
+		if (owner == "me")
+		{
+			query.ForOwner(User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "<unknown>");
+		}
+		else if (owner != null)
+		{
+			query.ForOwner(owner);
+		}
+
+		if (filter != null)
+		{
+			query.FilterByName(filter);
+		}
+
+		query.Paging((page ?? 0) * PageSize, PageSize);
+
+		return Ok(query.Get());
 	}
 
 	[HttpPost]

+ 22 - 1
src/RunnersMeet.Server/Persistence/TracksQuery.cs

@@ -1,3 +1,4 @@
+using LiteDB;
 using RunnersMeet.Server.Domain;
 
 namespace RunnersMeet.Server.Persistence;
@@ -5,14 +6,34 @@ namespace RunnersMeet.Server.Persistence;
 public class TracksQuery : ITracksQuery
 {
 	private readonly IDatabase _database;
+	private readonly ILiteQueryable<Track> _query;
 
 	public TracksQuery(IDatabase database)
 	{
 		_database = database;
+		_query = _database.Tracks.Query();
+	}
+
+	public TracksQuery ForOwner(string owner)
+	{
+		_query.Where(track => track.Owner == owner);
+		return this;
+	}
+
+	public TracksQuery FilterByName(string value)
+	{
+		_query.Where(track => track.DisplayName.Contains(value));
+		return this;
+	}
+
+	public TracksQuery Paging(int offset, int limit)
+	{
+		_query.OrderBy(track => track.DisplayName).Offset(offset).Limit(limit);
+		return this;
 	}
 
 	public IEnumerable<Track> Get()
 	{
-		return _database.Tracks.Query().OrderBy(t => t.DisplayName).ToEnumerable();
+		return _query.ToEnumerable();
 	}
 }