diff options
24 files changed, 863 insertions, 244 deletions
diff --git a/BackEnd/Timeline.Tests/IntegratedTests2/HttpClientTestExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests2/HttpClientTestExtensions.cs index cd7daf3e..48496853 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests2/HttpClientTestExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests2/HttpClientTestExtensions.cs @@ -12,7 +12,7 @@ namespace Timeline.Tests.IntegratedTests2 public static class HttpClientTestExtensions
{
- public static async Task<HttpResponseMessage> TestSendAsync(this HttpClient client, HttpMethod method, string url, HttpContent? body = null, HttpStatusCode expectedStatusCode = HttpStatusCode.OK, RequestSetupAsync? requestSetup = null)
+ public static async Task<HttpResponseMessage> TestSendAsync(this HttpClient client, HttpMethod method, string url, HttpContent? body = null, HttpStatusCode? expectedStatusCode = null, RequestSetupAsync? requestSetup = null)
{
using var req = new HttpRequestMessage
{
@@ -23,7 +23,14 @@ namespace Timeline.Tests.IntegratedTests2 var task = requestSetup?.Invoke(req);
if (task is not null) await task;
var res = await client.SendAsync(req);
- res.StatusCode.Should().Be(expectedStatusCode);
+ if (expectedStatusCode is null) + { + ((int)res.StatusCode).Should().BeGreaterThanOrEqualTo(200).And.BeLessThan(300);
+ }
+ else + { + res.StatusCode.Should().Be(expectedStatusCode.Value);
+ }
return res;
}
@@ -34,13 +41,13 @@ namespace Timeline.Tests.IntegratedTests2 return body!;
}
- public static async Task TestJsonSendAsync(this HttpClient client, HttpMethod method, string url, object? jsonBody = null, HttpStatusCode expectedStatusCode = HttpStatusCode.OK, RequestSetupAsync? requestSetup = null)
+ public static async Task TestJsonSendAsync(this HttpClient client, HttpMethod method, string url, object? jsonBody = null, HttpStatusCode? expectedStatusCode = null, RequestSetupAsync? requestSetup = null)
{
using JsonContent? reqContent = jsonBody is null ? null : JsonContent.Create(jsonBody, options: CommonJsonSerializeOptions.Options);
await client.TestSendAsync(method, url, reqContent, expectedStatusCode, requestSetup);
}
- public static async Task<T> TestJsonSendAsync<T>(this HttpClient client, HttpMethod method, string url, object? jsonBody = null, HttpStatusCode expectedStatusCode = HttpStatusCode.OK, RequestSetupAsync? requestSetup = null)
+ public static async Task<T> TestJsonSendAsync<T>(this HttpClient client, HttpMethod method, string url, object? jsonBody = null, HttpStatusCode? expectedStatusCode = null, RequestSetupAsync? requestSetup = null)
{
using JsonContent? reqContent = jsonBody == null ? null : JsonContent.Create(jsonBody, options: CommonJsonSerializeOptions.Options);
var res = await client.TestSendAsync(method, url, reqContent, expectedStatusCode, requestSetup);
diff --git a/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest.cs b/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest.cs index fcf69364..903175c3 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest.cs @@ -23,14 +23,13 @@ namespace Timeline.Tests.IntegratedTests2 { Name = "hello" }, expectedStatusCode: HttpStatusCode.Created); - } [Fact] - public async Task CreateAndGet() + public async Task CreateGetList() { using var client = CreateClientAsUser(); - var a = await client.TestJsonSendAsync<TimelineBookmark>(HttpMethod.Post, "users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + var a = await client.TestJsonSendAsync<TimelineBookmark>(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest { TimelineOwner = "user", TimelineName = "hello" @@ -40,10 +39,108 @@ namespace Timeline.Tests.IntegratedTests2 a.TimelineName.Should().Be("hello"); a.Position.Should().Be(1); - var b = await client.TestJsonSendAsync<TimelineBookmark>(HttpMethod.Get, "users/user/bookmarks/1"); + var b = await client.TestJsonSendAsync<TimelineBookmark>(HttpMethod.Get, "v2/users/user/bookmarks/1"); b.TimelineName.Should().Be("hello"); b.TimelineOwner.Should().Be("user"); b.Position.Should().Be(1); + + var c = await client.TestJsonSendAsync<Page<TimelineBookmark>>(HttpMethod.Get, "v2/users/user/bookmarks"); + c.TotalCount.Should().Be(1); + c.Items.Should().ContainSingle(); + c.Items[0].TimelineOwner.Should().Be("user"); + c.Items[0].TimelineName.Should().Be("hello"); + c.Items[0].Position.Should().Be(1); + } + + [Fact] + public async Task ListGetNotExist() + { + using var client = CreateClientAsUser(); + + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/notexist/bookmarks", expectedStatusCode: HttpStatusCode.NotFound); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/notexist/bookmarks/1", expectedStatusCode: HttpStatusCode.NotFound); + } + + [Fact] + public async Task CreateUserNotExist() + { + using var client = CreateClientAsUser(); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/notexist/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.NotFound); + } + + [Fact] + public async Task CreateTimelineNotExist() + { + using var client = CreateClientAsUser(); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "notexist" + }, expectedStatusCode: HttpStatusCode.UnprocessableEntity); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "notexist", + TimelineName = "notexist" + }, expectedStatusCode: HttpStatusCode.UnprocessableEntity); + } + + [Fact] + public async Task CreateAlreadyExist() + { + using var client = CreateClientAsUser(); + + await client.TestJsonSendAsync<TimelineBookmark>(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.Created); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.UnprocessableEntity); + } + + [Fact] + public async Task AnonymousCreateUnauthorized() + { + using var client = CreateDefaultClient(); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task OtherCreateForbid() + { + using var client = CreateClientAsUser(); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/admin/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.Forbidden); + } + + [Fact] + public async Task AdminCanCreate() + { + using var client = CreateClientAsAdmin(); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }); } } } diff --git a/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest2.cs b/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest2.cs new file mode 100644 index 00000000..b701e4eb --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest2.cs @@ -0,0 +1,138 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Timeline.Models; +using Timeline.Models.Http; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests.IntegratedTests2 +{ + public class TimelineBookmarkTest2 : IntegratedTestBase + { + public TimelineBookmarkTest2(ITestOutputHelper testOutput) : base(testOutput) + { + } + + protected override async Task OnInitializeAsync() + { + using var client = CreateClientAsUser(); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/timelines", new HttpTimelineCreateRequest + { + Name = "hello" + }, expectedStatusCode: HttpStatusCode.Created); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.Created); + } + + private async Task ChangeVisibilityAsync(TimelineVisibility visibility) + { + using var client = CreateClientAsUser(); + await client.TestJsonSendAsync(HttpMethod.Put, "v2/users/user/bookmarks/visibility", new HttpTimelineBookmarkVisibility { Visibility = visibility }, expectedStatusCode: HttpStatusCode.NoContent); + } + + [Fact] + public async Task ChangeVisibilityShouldWork() + { + using var client = CreateClientAsUser(); + var a = await client.TestJsonSendAsync<HttpTimelineBookmarkVisibility>(HttpMethod.Get, "v2/users/user/bookmarks/visibility", expectedStatusCode: HttpStatusCode.OK); + a.Visibility.Should().Be(TimelineVisibility.Private); + + await client.TestJsonSendAsync(HttpMethod.Put, "v2/users/user/bookmarks/visibility", new HttpTimelineBookmarkVisibility { Visibility = TimelineVisibility.Register }, expectedStatusCode: HttpStatusCode.NoContent); + var b = await client.TestJsonSendAsync<HttpTimelineBookmarkVisibility>(HttpMethod.Get, "v2/users/user/bookmarks/visibility", expectedStatusCode: HttpStatusCode.OK); + b.Visibility.Should().Be(TimelineVisibility.Register); + + await client.TestJsonSendAsync(HttpMethod.Put, "v2/users/user/bookmarks/visibility", new HttpTimelineBookmarkVisibility { Visibility = TimelineVisibility.Public }, expectedStatusCode: HttpStatusCode.NoContent); + var c = await client.TestJsonSendAsync<HttpTimelineBookmarkVisibility>(HttpMethod.Get, "v2/users/user/bookmarks/visibility", expectedStatusCode: HttpStatusCode.OK); + c.Visibility.Should().Be(TimelineVisibility.Public); + } + + [Fact] + public async Task AnonymousCantSeePrivate() + { + using var client = CreateDefaultClient(); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.Forbidden); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.Forbidden); + } + + [Fact] + public async Task OtherUserCantSeePrivate() + { + await CreateUserAsync("user2", "user2pw"); + using var client = CreateClientWithToken(await CreateTokenWithCredentialAsync("user2", "user2pw")); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.Forbidden); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.Forbidden); + } + + [Fact] + public async Task AdminCanSeePrivate() + { + using var client = CreateClientAsAdmin(); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.OK); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.OK); + } + + [Fact] + public async Task AnonymousCantSeeRegister() + { + await ChangeVisibilityAsync(TimelineVisibility.Register); + using var client = CreateDefaultClient(); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.Forbidden); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.Forbidden); + } + + [Fact] + public async Task OtherUserCanSeeRegister() + { + await ChangeVisibilityAsync(TimelineVisibility.Register); + await CreateUserAsync("user2", "user2pw"); + using var client = CreateClientWithToken(await CreateTokenWithCredentialAsync("user2", "user2pw")); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.OK); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.OK); + } + + [Fact] + public async Task AdminCanSeeRegister() + { + await ChangeVisibilityAsync(TimelineVisibility.Register); + using var client = CreateClientAsAdmin(); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.OK); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.OK); + } + + [Fact] + public async Task AnonymousCanSeePublic() + { + await ChangeVisibilityAsync(TimelineVisibility.Public); + using var client = CreateDefaultClient(); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.OK); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.OK); + } + + [Fact] + public async Task OtherUserCanSeePublic() + { + await ChangeVisibilityAsync(TimelineVisibility.Public); + await CreateUserAsync("user2", "user2pw"); + using var client = CreateClientWithToken(await CreateTokenWithCredentialAsync("user2", "user2pw")); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.OK); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.OK); + } + + [Fact] + public async Task AdminCanSeePublic() + { + await ChangeVisibilityAsync(TimelineVisibility.Public); + using var client = CreateClientAsAdmin(); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks", expectedStatusCode: HttpStatusCode.OK); + await client.TestJsonSendAsync(HttpMethod.Get, "v2/users/user/bookmarks/1", expectedStatusCode: HttpStatusCode.OK); + } + } +} + diff --git a/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest3.cs b/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest3.cs new file mode 100644 index 00000000..f2329bd9 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest3.cs @@ -0,0 +1,192 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Timeline.Models; +using Timeline.Models.Http; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests.IntegratedTests2 +{ + public class TimelineBookmarkTest3 : IntegratedTestBase + { + public TimelineBookmarkTest3(ITestOutputHelper testOutput) : base(testOutput) + { + } + + protected override async Task OnInitializeAsync() + { + using var client = CreateClientAsUser(); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/timelines", new HttpTimelineCreateRequest + { + Name = "hello" + }, expectedStatusCode: HttpStatusCode.Created); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/timelines", new HttpTimelineCreateRequest + { + Name = "hello2" + }, expectedStatusCode: HttpStatusCode.Created); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.Created); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks", new HttpTimelineBookmarkCreateRequest + { + TimelineOwner = "user", + TimelineName = "hello2" + }, expectedStatusCode: HttpStatusCode.Created); + } + + [Fact] + public async Task DeleteTest() + { + using var client = CreateClientAsUser(); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/delete", new HttpTimelinebookmarkDeleteRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.NoContent); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/delete", new HttpTimelinebookmarkDeleteRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.NoContent); + } + + [Fact] + public async Task MoveTest() + { + using var client = CreateClientAsUser(); + var a = await client.TestJsonSendAsync<TimelineBookmark>(HttpMethod.Post, "v2/users/user/bookmarks/move", new HttpTimelineBookmarkMoveRequest + { + TimelineOwner = "user", + TimelineName = "hello", + Position = 2 + }); + a.Position.Should().Be(2); + + var b = await client.TestJsonSendAsync<TimelineBookmark>(HttpMethod.Get, "v2/users/user/bookmarks/2"); + b.TimelineOwner.Should().Be("user"); + b.TimelineName.Should().Be("hello"); + + await client.TestJsonSendAsync<TimelineBookmark>(HttpMethod.Post, "v2/users/user/bookmarks/move", new HttpTimelineBookmarkMoveRequest + { + TimelineOwner = "user", + TimelineName = "hello", + }, expectedStatusCode: HttpStatusCode.UnprocessableEntity); + } + + [Fact] + public async Task DeleteMoveNotExist() + { + using var client = CreateClientAsUser(); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/delete", new HttpTimelinebookmarkDeleteRequest + { + TimelineOwner = "notexist", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.UnprocessableEntity); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/delete", new HttpTimelinebookmarkDeleteRequest + { + TimelineOwner = "user", + TimelineName = "notexist" + }, expectedStatusCode: HttpStatusCode.UnprocessableEntity); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/move", new HttpTimelineBookmarkMoveRequest + { + TimelineOwner = "notexist", + TimelineName = "hello", + Position = 2 + + }, expectedStatusCode: HttpStatusCode.UnprocessableEntity); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/move", new HttpTimelineBookmarkMoveRequest + { + TimelineOwner = "user", + TimelineName = "notexist", + Position = 2 + }, expectedStatusCode: HttpStatusCode.UnprocessableEntity); + } + + [Fact] + public async Task DeleteMoveNotLogin() + { + using var client = CreateDefaultClient(); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/delete", new HttpTimelinebookmarkDeleteRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.Unauthorized); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/move", new HttpTimelineBookmarkMoveRequest + { + TimelineOwner = "user", + TimelineName = "hello", + Position = 2 + }, expectedStatusCode: HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task DeleteMoveForbid() + { + await CreateUserAsync("user2", "user2pw"); + using var client = CreateClientWithToken(await CreateTokenWithCredentialAsync("user2", "user2pw")); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/delete", new HttpTimelinebookmarkDeleteRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.Forbidden); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/move", new HttpTimelineBookmarkMoveRequest + { + TimelineOwner = "user", + TimelineName = "hello", + Position = 2 + }, expectedStatusCode: HttpStatusCode.Forbidden); + } + + [Fact] + public async Task DeleteAdmin() + { + using var client = CreateClientAsAdmin(); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/delete", new HttpTimelinebookmarkDeleteRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.NoContent); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/notexist/bookmarks/delete", new HttpTimelinebookmarkDeleteRequest + { + TimelineOwner = "user", + TimelineName = "hello" + }, expectedStatusCode: HttpStatusCode.NotFound); + } + + [Fact] + public async Task MoveAdmin() + { + using var client = CreateClientAsAdmin(); + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/user/bookmarks/move", new HttpTimelineBookmarkMoveRequest + { + TimelineOwner = "user", + TimelineName = "hello", + Position = 2 + }, expectedStatusCode: HttpStatusCode.OK); + + await client.TestJsonSendAsync(HttpMethod.Post, "v2/users/notexist/bookmarks/move", new HttpTimelineBookmarkMoveRequest + { + TimelineOwner = "user", + TimelineName = "hello", + Position = 2 + }, expectedStatusCode: HttpStatusCode.NotFound); + } + } +} + diff --git a/BackEnd/Timeline.Tests/IntegratedTests2/TimelinePostTest1.cs b/BackEnd/Timeline.Tests/IntegratedTests2/TimelinePostTest.cs index d06da9d9..53a98eae 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests2/TimelinePostTest1.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests2/TimelinePostTest.cs @@ -12,9 +12,9 @@ using Xunit.Abstractions; namespace Timeline.Tests.IntegratedTests2 { - public class TimelinePostTest1 : IntegratedTestBase + public class TimelinePostTest : IntegratedTestBase { - public TimelinePostTest1(ITestOutputHelper testOutput) : base(testOutput) + public TimelinePostTest(ITestOutputHelper testOutput) : base(testOutput) { } diff --git a/BackEnd/Timeline/Controllers/TimelineBookmark1Controller.cs b/BackEnd/Timeline/Controllers/TimelineBookmark1Controller.cs deleted file mode 100644 index 73d2078f..00000000 --- a/BackEnd/Timeline/Controllers/TimelineBookmark1Controller.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Services.Api; -using Timeline.Services.Timeline; -using Timeline.Services.User; - -namespace Timeline.Controllers -{ - [ApiController] - [Route("users/{username}/bookmarks")] - public class TimelineBookmark1Controller : MyControllerBase - { - private readonly IUserService _userService; - private readonly ITimelineService _timelineService; - private readonly ITimelineBookmarkService1 _timelineBookmarkService; - - public TimelineBookmark1Controller(IUserService userService, ITimelineService timelineService, ITimelineBookmarkService1 timelineBookmarkService) - { - _userService = userService; - _timelineService = timelineService; - _timelineBookmarkService = timelineBookmarkService; - } - - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - [HttpGet] - public async Task<ActionResult<Page<TimelineBookmark>>> ListAsync([FromRoute] string username, [FromQuery] int? page, [FromQuery] int? pageSize) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && !await _timelineBookmarkService.CanReadBookmarksAsync(userId, GetOptionalAuthUserId())) - { - return Forbid(); - } - return await _timelineBookmarkService.GetBookmarksAsync(userId, page ?? 1, pageSize ?? 20); - } - - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - [HttpGet("{index}")] - public async Task<ActionResult<TimelineBookmark>> GetAsync([FromRoute] string username, [FromRoute] int index) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && !await _timelineBookmarkService.CanReadBookmarksAsync(userId, GetOptionalAuthUserId())) - { - return Forbid(); - } - return await _timelineBookmarkService.GetBookmarkAtAsync(userId, index); - } - - [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - [Authorize] - public async Task<ActionResult<TimelineBookmark>> CreateAsync([FromRoute] string username, [FromBody] HttpTimelineBookmarkCreateRequest body) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) - { - return Forbid(); - } - var timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); - var bookmark = await _timelineBookmarkService.AddBookmarkAsync(userId, timelineId, body.Position); - return CreatedAtAction("Get", new { username, index = bookmark.Position }, bookmark); - } - } -} diff --git a/BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs b/BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs new file mode 100644 index 00000000..c2130b5a --- /dev/null +++ b/BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs @@ -0,0 +1,176 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Api; +using Timeline.Services.Timeline; +using Timeline.Services.User; + +namespace Timeline.Controllers +{ + [ApiController] + [Route("v2/users/{username}/bookmarks")] + public class TimelineBookmarkV2Controller : MyControllerBase + { + private readonly IUserService _userService; + private readonly ITimelineService _timelineService; + private readonly ITimelineBookmarkService1 _timelineBookmarkService; + + public TimelineBookmarkV2Controller(IUserService userService, ITimelineService timelineService, ITimelineBookmarkService1 timelineBookmarkService) + { + _userService = userService; + _timelineService = timelineService; + _timelineBookmarkService = timelineBookmarkService; + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [HttpGet] + public async Task<ActionResult<Page<TimelineBookmark>>> ListAsync([FromRoute][Username] string username, [FromQuery] int? page, [FromQuery] int? pageSize) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && !await _timelineBookmarkService.CanReadBookmarksAsync(userId, GetOptionalAuthUserId())) + { + return Forbid(); + } + return await _timelineBookmarkService.GetBookmarksAsync(userId, page ?? 1, pageSize ?? 20); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [HttpGet("{index}")] + public async Task<ActionResult<TimelineBookmark>> GetAsync([FromRoute][Username] string username, [FromRoute] int index) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && !await _timelineBookmarkService.CanReadBookmarksAsync(userId, GetOptionalAuthUserId())) + { + return Forbid(); + } + return await _timelineBookmarkService.GetBookmarkAtAsync(userId, index); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [Authorize] + [HttpPost] + public async Task<ActionResult<TimelineBookmark>> CreateAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkCreateRequest body) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) + { + return Forbid(); + } + long timelineId; + try + { + timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); + } + catch (EntityNotExistException) + { + return UnprocessableEntity(); + } + var bookmark = await _timelineBookmarkService.AddBookmarkAsync(userId, timelineId, body.Position); + return CreatedAtAction("Get", new { username, index = bookmark.Position }, bookmark); + } + + [Authorize] + [HttpPost("delete")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult> DeleteAsync([FromRoute][Username] string username, [FromBody] HttpTimelinebookmarkDeleteRequest body) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) + { + return Forbid(); + } + + long timelineId; + try + { + timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); + } + catch (EntityNotExistException) + { + return UnprocessableEntity(); + } + + await _timelineBookmarkService.DeleteBookmarkAsync(userId, timelineId); + + return NoContent(); + } + + [Authorize] + [HttpPost("move")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult<TimelineBookmark>> MoveAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkMoveRequest body) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) + { + return Forbid(); + } + + long timelineId; + try + { + timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); + } + catch (EntityNotExistException) + { + return UnprocessableEntity(); + } + + var bookmark = await _timelineBookmarkService.MoveBookmarkAsync(userId, timelineId, body.Position!.Value); + + return Ok(bookmark); + } + + [HttpGet("visibility")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult<HttpTimelineBookmarkVisibility>> GetVisibilityAsync([FromRoute][Username] string username) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + var visibility = await _timelineBookmarkService.GetBookmarkVisibilityAsync(userId); + return Ok(new HttpTimelineBookmarkVisibility { Visibility = visibility }); + } + + [HttpPut("visibility")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult> PutVisibilityAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkVisibility body) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) + { + return Forbid(); + } + await _timelineBookmarkService.SetBookmarkVisibilityAsync(userId, body.Visibility); + return NoContent(); + } + } +} diff --git a/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs b/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs index 6e4b265f..435ffece 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; diff --git a/BackEnd/Timeline/Models/Http/HttpTimelineBookmarkMoveRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelineBookmarkMoveRequest.cs new file mode 100644 index 00000000..5be7fd00 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpTimelineBookmarkMoveRequest.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + public class HttpTimelineBookmarkMoveRequest + { + [Required] + [Username] + public string TimelineOwner { get; set; } = default!; + + [Required] + [TimelineName] + public string TimelineName { get; set; } = default!; + + [Required] + public int? Position { get; set; } + } +} + diff --git a/BackEnd/Timeline/Models/Http/HttpTimelineBookmarkVisibility.cs b/BackEnd/Timeline/Models/Http/HttpTimelineBookmarkVisibility.cs new file mode 100644 index 00000000..d19c12f6 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpTimelineBookmarkVisibility.cs @@ -0,0 +1,12 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Timeline.Models.Http +{ + public class HttpTimelineBookmarkVisibility + { + [Required] + public TimelineVisibility Visibility { get; set; } + } +} + diff --git a/BackEnd/Timeline/Models/Http/HttpTimelinebookmarkDeleteRequest.cs b/BackEnd/Timeline/Models/Http/HttpTimelinebookmarkDeleteRequest.cs new file mode 100644 index 00000000..ab45f976 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpTimelinebookmarkDeleteRequest.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + public class HttpTimelinebookmarkDeleteRequest + { + [Required] + [Username] + public string TimelineOwner { get; set; } = default!; + + [Required] + [TimelineName] + public string TimelineName { get; set; } = default!; + } +} + diff --git a/BackEnd/Timeline/Services/Api/TimelineBookmarkService1.cs b/BackEnd/Timeline/Services/Api/TimelineBookmarkService1.cs index 4b52d61c..648ea9ff 100644 --- a/BackEnd/Timeline/Services/Api/TimelineBookmarkService1.cs +++ b/BackEnd/Timeline/Services/Api/TimelineBookmarkService1.cs @@ -205,7 +205,7 @@ namespace Timeline.Services.Api await transaction.CommitAsync(); } - return new TimelineBookmark(user.Username, timeline.Name is null ? "self" : timeline.Name, (int)entity.Rank); + return new TimelineBookmark(user.Username, timeline.Name is null ? "self" : timeline.Name, position); } public async Task SetBookmarkVisibilityAsync(long userId, TimelineVisibility visibility) diff --git a/FrontEnd/package.json b/FrontEnd/package.json index b7db3306..358eadc1 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -43,6 +43,7 @@ "start": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
+ "typecheck": "tsc",
"lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx",
"lint:fix": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx --fix"
},
diff --git a/FrontEnd/src/common.ts b/FrontEnd/src/common.ts index 91d58562..b819d209 100644 --- a/FrontEnd/src/common.ts +++ b/FrontEnd/src/common.ts @@ -29,3 +29,5 @@ export function convertI18nText( return text.value; } } + +export const highlightTimelineUsername = "crupest"; diff --git a/FrontEnd/src/http/bookmark.ts b/FrontEnd/src/http/bookmark.ts index 3e5be229..382543ff 100644 --- a/FrontEnd/src/http/bookmark.ts +++ b/FrontEnd/src/http/bookmark.ts @@ -1,36 +1,93 @@ -import { axios, apiBaseUrl, extractResponseData } from "./common"; +import { applyQueryParameters } from "@/utilities/url"; +import { axios, apiBaseUrl, extractResponseData, Page } from "./common"; -import { HttpTimelineInfo } from "./timeline"; - -export interface HttpHighlightMoveRequest { - timeline: string; - newPosition: number; +export interface TimelineBookmark { + timelineOwner: string; + timelineName: string; + position: number; } export interface IHttpBookmarkClient { - list(): Promise<HttpTimelineInfo[]>; - put(timeline: string): Promise<void>; - delete(timeline: string): Promise<void>; - move(req: HttpHighlightMoveRequest): Promise<void>; + list( + username: string, + page?: number, + pageSize?: number + ): Promise<Page<TimelineBookmark>>; + post( + username: string, + timelineOwner: string, + timelineName: string + ): Promise<TimelineBookmark>; + delete( + username: string, + timelineOwner: string, + timelineName: string + ): Promise<void>; + move( + username: string, + timelineOwner: string, + timelineName: string, + position: number + ): Promise<TimelineBookmark>; } export class HttpHighlightClient implements IHttpBookmarkClient { - list(): Promise<HttpTimelineInfo[]> { + list( + username: string, + page?: number, + pageSize?: number + ): Promise<Page<TimelineBookmark>> { + const url = applyQueryParameters( + `${apiBaseUrl}/v2/users/${username}/bookmarks`, + { page, pageSize } + ); + + return axios.get<Page<TimelineBookmark>>(url).then(extractResponseData); + } + + post( + username: string, + timelineOwner: string, + timelineName: string + ): Promise<TimelineBookmark> { + const url = `${apiBaseUrl}/v2/users/${username}/bookmarks`; + return axios - .get<HttpTimelineInfo[]>(`${apiBaseUrl}/bookmarks`) + .post<TimelineBookmark>(url, { + timelineOwner, + timelineName, + }) .then(extractResponseData); } - put(timeline: string): Promise<void> { - return axios.put(`${apiBaseUrl}/bookmarks/${timeline}`).then(); - } + delete( + username: string, + timelineOwner: string, + timelineName: string + ): Promise<void> { + const url = `${apiBaseUrl}/v2/users/${username}/bookmarks/delete`; - delete(timeline: string): Promise<void> { - return axios.delete(`${apiBaseUrl}/bookmarks/${timeline}`).then(); + return axios.post(url, { + timelineOwner, + timelineName, + }); } - move(req: HttpHighlightMoveRequest): Promise<void> { - return axios.post(`${apiBaseUrl}/bookmarkop/move`, req).then(); + move( + username: string, + timelineOwner: string, + timelineName: string, + position: number + ): Promise<TimelineBookmark> { + const url = `${apiBaseUrl}/v2/users/${username}/bookmarks/move`; + + return axios + .post<TimelineBookmark>(url, { + timelineOwner, + timelineName, + position, + }) + .then(extractResponseData); } } diff --git a/FrontEnd/src/http/common.ts b/FrontEnd/src/http/common.ts index e1672985..0a27b908 100644 --- a/FrontEnd/src/http/common.ts +++ b/FrontEnd/src/http/common.ts @@ -212,3 +212,11 @@ export function convertToBlobWithEtag(res: AxiosResponse<Blob>): BlobWithEtag { export function extractEtag(res: AxiosResponse): string { return (res.headers as Record<"etag", string>)["etag"]; } + +export interface Page<T> { + pageNumber: number; + pageSize: number; + totalPageCount: number; + totalCount: number; + items: T[]; +} diff --git a/FrontEnd/src/http/highlight.ts b/FrontEnd/src/http/highlight.ts deleted file mode 100644 index fddf0729..00000000 --- a/FrontEnd/src/http/highlight.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { axios, apiBaseUrl, extractResponseData } from "./common"; - -import { HttpTimelineInfo } from "./timeline"; - -export interface HttpHighlightMoveRequest { - timeline: string; - newPosition: number; -} - -export interface IHttpHighlightClient { - list(): Promise<HttpTimelineInfo[]>; - put(timeline: string): Promise<void>; - delete(timeline: string): Promise<void>; - move(req: HttpHighlightMoveRequest): Promise<void>; -} - -export class HttpHighlightClient implements IHttpHighlightClient { - list(): Promise<HttpTimelineInfo[]> { - return axios - .get<HttpTimelineInfo[]>(`${apiBaseUrl}/highlights`) - .then(extractResponseData); - } - - put(timeline: string): Promise<void> { - return axios.put(`${apiBaseUrl}/highlights/${timeline}`).then(); - } - - delete(timeline: string): Promise<void> { - return axios.delete(`${apiBaseUrl}/highlights/${timeline}`).then(); - } - - move(req: HttpHighlightMoveRequest): Promise<void> { - return axios.post(`${apiBaseUrl}/highlightop/move`, req).then(); - } -} - -let client: IHttpHighlightClient = new HttpHighlightClient(); - -export function getHttpHighlightClient(): IHttpHighlightClient { - return client; -} - -export function setHttpHighlightClient( - newClient: IHttpHighlightClient -): IHttpHighlightClient { - const old = client; - client = newClient; - return old; -} diff --git a/FrontEnd/src/http/timeline.ts b/FrontEnd/src/http/timeline.ts index 49a7b8f2..65717a83 100644 --- a/FrontEnd/src/http/timeline.ts +++ b/FrontEnd/src/http/timeline.ts @@ -8,6 +8,7 @@ import { extractResponseData, convertToIfStatusCodeIs, getHttpToken, + Page, } from "./common"; import { HttpUser } from "./user"; @@ -115,7 +116,7 @@ export interface IHttpTimelineClient { listPost( ownerUsername: string, timelineName: string - ): Promise<HttpTimelinePostInfo[]>; + ): Promise<Page<HttpTimelinePostInfo>>; generatePostDataUrl( ownerUsername: string, timelineName: string, @@ -217,9 +218,9 @@ export class HttpTimelineClient implements IHttpTimelineClient { listPost( ownerUsername: string, timelineName: string - ): Promise<HttpTimelinePostInfo[]> { + ): Promise<Page<HttpTimelinePostInfo>> { return axios - .get<HttpTimelinePostInfo[]>( + .get<Page<HttpTimelinePostInfo>>( `${apiBaseUrl}/v2/timelines/${ownerUsername}/${timelineName}/posts` ) .then(extractResponseData); diff --git a/FrontEnd/src/views/center/CenterBoards.tsx b/FrontEnd/src/views/center/CenterBoards.tsx index 392c2d08..e95f4cb8 100644 --- a/FrontEnd/src/views/center/CenterBoards.tsx +++ b/FrontEnd/src/views/center/CenterBoards.tsx @@ -1,12 +1,13 @@ import React from "react"; import { useTranslation } from "react-i18next"; +import { highlightTimelineUsername } from "@/common"; + import { pushAlert } from "@/services/alert"; import { useUserLoggedIn } from "@/services/user"; import { getHttpTimelineClient } from "@/http/timeline"; import { getHttpBookmarkClient } from "@/http/bookmark"; -import { getHttpHighlightClient } from "@/http/highlight"; import TimelineBoard from "./TimelineBoard"; @@ -23,11 +24,15 @@ const CenterBoards: React.FC = () => { <div className="col col-12 my-2"> <TimelineBoard title={t("home.bookmarkTimeline")} - load={() => getHttpBookmarkClient().list()} + load={() => + getHttpBookmarkClient() + .list(user.username) + .then((p) => p.items) + } editHandler={{ - onDelete: (timeline) => { + onDelete: (owner, timeline) => { return getHttpBookmarkClient() - .delete(timeline) + .delete(user.username, owner, timeline) .catch((e) => { pushAlert({ message: "home.message.deleteBookmarkFail", @@ -36,10 +41,13 @@ const CenterBoards: React.FC = () => { throw e; }); }, - onMove: (timeline, index, offset) => { + onMove: (owner, timeline, index, offset) => { return getHttpBookmarkClient() .move( - { timeline, newPosition: index + offset + 1 } // +1 because backend contract: index starts at 1 + user.username, + owner, + timeline, + index + offset + 1 // +1 because backend contract: index starts at 1 ) .catch((e) => { pushAlert({ @@ -47,7 +55,8 @@ const CenterBoards: React.FC = () => { type: "danger", }); throw e; - }); + }) + .then(); }, }} /> @@ -55,13 +64,17 @@ const CenterBoards: React.FC = () => { <div className="col col-12 my-2"> <TimelineBoard title={t("home.highlightTimeline")} - load={() => getHttpHighlightClient().list()} + load={() => + getHttpBookmarkClient() + .list(highlightTimelineUsername) + .then((p) => p.items) + } editHandler={ - user.hasHighlightTimelineAdministrationPermission + user.username === highlightTimelineUsername ? { - onDelete: (timeline) => { - return getHttpHighlightClient() - .delete(timeline) + onDelete: (owner, timeline) => { + return getHttpBookmarkClient() + .delete(highlightTimelineUsername, owner, timeline) .catch((e) => { pushAlert({ message: "home.message.deleteHighlightFail", @@ -70,18 +83,22 @@ const CenterBoards: React.FC = () => { throw e; }); }, - onMove: (timeline, index, offset) => { - return getHttpHighlightClient() + onMove: (owner, timeline, index, offset) => { + return getHttpBookmarkClient() .move( - { timeline, newPosition: index + offset + 1 } // +1 because backend contract: index starts at 1 + highlightTimelineUsername, + owner, + timeline, + index + offset + 1 // +1 because backend contract: index starts at 1 ) .catch((e) => { pushAlert({ - message: "home.message.moveHighlightFail", + message: "home.message.moveBookmarkFail", type: "danger", }); throw e; - }); + }) + .then(); }, } : undefined @@ -94,7 +111,15 @@ const CenterBoards: React.FC = () => { <TimelineBoard title={t("home.relatedTimeline")} load={() => - getHttpTimelineClient().listTimeline({ relate: user.username }) + getHttpTimelineClient() + .listTimeline({ relate: user.username }) + .then((l) => + l.map((t, index) => ({ + timelineOwner: t.owner.username, + timelineName: t.nameV2, + position: index + 1, + })) + ) } /> </div> diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index f8dc4bfd..45b4a8a5 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -2,7 +2,7 @@ import React from "react"; import classnames from "classnames"; import { Link } from "react-router-dom"; -import { HttpTimelineInfo } from "@/http/timeline"; +import { TimelineBookmark } from "@/http/bookmark"; import TimelineLogo from "../common/TimelineLogo"; import LoadFailReload from "../common/LoadFailReload"; @@ -11,7 +11,7 @@ import Card from "../common/Card"; import Spinner from "../common/Spinner"; interface TimelineBoardItemProps { - timeline: HttpTimelineInfo; + timeline: TimelineBookmark; // In height. offset?: number; // In px. @@ -33,15 +33,12 @@ const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({ offset, actions, }) => { - const { title } = timeline; - const content = ( <> <TimelineLogo className="icon" /> - <span className="title">{title}</span> - <small className="ms-2 cru-color-secondary"> - {timeline.owner.username}/{timeline.nameV2} - </small> + <span className="title"> + {timeline.timelineOwner}/{timeline.timelineName} + </span> <span className="flex-grow-1"></span> {actions != null ? ( <div className="right"> @@ -83,7 +80,7 @@ const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({ return actions == null ? ( <Link - to={`${timeline.owner.username}/${timeline.nameV2}`} + to={`${timeline.timelineOwner}/${timeline.timelineName}`} className="timeline-board-item" > {content} @@ -96,7 +93,7 @@ const TimelineBoardItem: React.FC<TimelineBoardItemProps> = ({ }; interface TimelineBoardItemContainerProps { - timelines: HttpTimelineInfo[]; + timelines: TimelineBookmark[]; editHandler?: { // offset may exceed index range plusing index. onMove: ( @@ -156,7 +153,7 @@ const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({ return ( <TimelineBoardItem - key={`${timeline.owner.username}/${timeline.nameV2}`} + key={timeline.timelineOwner + "/" + timeline.timelineName} timeline={timeline} offset={offset} arbitraryOffset={arbitraryOffset} @@ -165,8 +162,8 @@ const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({ ? { onDelete: () => { editHandler.onDelete( - timeline.owner.username, - timeline.nameV2 + timeline.timelineOwner, + timeline.timelineName ); }, onMove: { @@ -192,8 +189,8 @@ const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({ moveState.offset / height ); editHandler.onMove( - timeline.owner.username, - timeline.nameV2, + timeline.timelineOwner, + timeline.timelineName, moveState.index, offsetCount ); @@ -214,7 +211,7 @@ const TimelineBoardItemContainer: React.FC<TimelineBoardItemContainerProps> = ({ interface TimelineBoardUIProps { title?: string; state: "offline" | "loading" | "loaded"; - timelines: HttpTimelineInfo[]; + timelines: TimelineBookmark[]; onReload: () => void; className?: string; editHandler?: { @@ -299,10 +296,15 @@ const TimelineBoardUI: React.FC<TimelineBoardUIProps> = (props) => { export interface TimelineBoardProps { title?: string; className?: string; - load: () => Promise<HttpTimelineInfo[]>; + load: () => Promise<TimelineBookmark[]>; editHandler?: { - onMove: (timeline: string, index: number, offset: number) => Promise<void>; - onDelete: (timeline: string) => Promise<void>; + onMove: ( + owner: string, + timeline: string, + index: number, + offset: number + ) => Promise<void>; + onDelete: (owner: string, timeline: string) => Promise<void>; }; } @@ -315,7 +317,7 @@ const TimelineBoard: React.FC<TimelineBoardProps> = ({ const [state, setState] = React.useState<"offline" | "loading" | "loaded">( "loading" ); - const [timelines, setTimelines] = React.useState<HttpTimelineInfo[]>([]); + const [timelines, setTimelines] = React.useState<TimelineBookmark[]>([]); React.useEffect(() => { let subscribe = true; @@ -354,20 +356,23 @@ const TimelineBoard: React.FC<TimelineBoardProps> = ({ const [t] = newTimelines.splice(index, 1); newTimelines.splice(index + offset, 0, t); setTimelines(newTimelines); - editHandler.onMove(timeline, index, offset).then(null, () => { - setTimelines(timelines); - }); + editHandler + .onMove(owner, timeline, index, offset) + .then(null, () => { + setTimelines(timelines); + }); }, onDelete: (owner, timeline) => { const newTimelines = timelines.slice(); newTimelines.splice( timelines.findIndex( - (t) => t.owner.username === owner && t.nameV2 === timeline + (t) => + t.timelineOwner === owner && t.timelineName === timeline ), 1 ); setTimelines(newTimelines); - editHandler.onDelete(timeline).then(null, () => { + editHandler.onDelete(owner, timeline).then(null, () => { setTimelines(timelines); }); }, diff --git a/FrontEnd/src/views/home/TimelineListView.tsx b/FrontEnd/src/views/home/TimelineListView.tsx index 2f283e1c..b26f1f70 100644 --- a/FrontEnd/src/views/home/TimelineListView.tsx +++ b/FrontEnd/src/views/home/TimelineListView.tsx @@ -1,13 +1,13 @@ import React from "react"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; import { convertI18nText, I18nText } from "@/common"; -import { HttpTimelineInfo } from "@/http/timeline"; -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; +import { TimelineBookmark } from "@/http/bookmark"; interface TimelineListItemProps { - timeline: HttpTimelineInfo; + timeline: TimelineBookmark; } const TimelineListItem: React.FC<TimelineListItemProps> = ({ timeline }) => { @@ -21,12 +21,9 @@ const TimelineListItem: React.FC<TimelineListItemProps> = ({ timeline }) => { /> </svg> <div> - <div>{timeline.title}</div> - <div> - <small className="text-secondary">{timeline.description}</small> - </div> + {timeline.timelineOwner}/{timeline.timelineName} </div> - <Link to={`${timeline.owner.username}/${timeline.nameV2}`}> + <Link to={`${timeline.timelineOwner}/${timeline.timelineName}`}> <i className="icon-button bi-arrow-right ms-3" /> </Link> </div> @@ -60,7 +57,7 @@ const TimelineListArrow: React.FC = () => { interface TimelineListViewProps { headerText?: I18nText; - timelines?: HttpTimelineInfo[]; + timelines?: TimelineBookmark[]; } const TimelineListView: React.FC<TimelineListViewProps> = ({ @@ -83,7 +80,12 @@ const TimelineListView: React.FC<TimelineListViewProps> = ({ <h3>{convertI18nText(headerText, t)}</h3> </div> {timelines != null - ? timelines.map((t) => <TimelineListItem key={t.nameV2} timeline={t} />) + ? timelines.map((t) => ( + <TimelineListItem + key={`${t.timelineOwner}/${t.timelineName}`} + timeline={t} + /> + )) : null} <TimelineListArrow /> </div> diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx index 9533ea5a..d734b2b7 100644 --- a/FrontEnd/src/views/home/index.tsx +++ b/FrontEnd/src/views/home/index.tsx @@ -1,8 +1,10 @@ import React from "react"; import { useNavigate } from "react-router-dom"; -import { HttpTimelineInfo } from "@/http/timeline"; -import { getHttpHighlightClient } from "@/http/highlight"; +import { highlightTimelineUsername } from "@/common"; + +import { Page } from "@/http/common"; +import { getHttpBookmarkClient, TimelineBookmark } from "@/http/bookmark"; import SearchInput from "../common/SearchInput"; import TimelineListView from "./TimelineListView"; @@ -25,14 +27,14 @@ const HomeV2: React.FC = () => { "loading" | "done" | "error" >("loading"); const [highlightTimelines, setHighlightTimelines] = React.useState< - HttpTimelineInfo[] | undefined + Page<TimelineBookmark> | undefined >(); React.useEffect(() => { if (highlightTimelineState === "loading") { let subscribe = true; - void getHttpHighlightClient() - .list() + void getHttpBookmarkClient() + .list(highlightTimelineUsername) .then( (data) => { if (subscribe) { @@ -67,7 +69,7 @@ const HomeV2: React.FC = () => { <WebsiteIntroduction className="m-2" /> <TimelineListView headerText={highlightTimelineMessageMap[highlightTimelineState]} - timelines={highlightTimelines} + timelines={highlightTimelines?.items} /> </> ); diff --git a/FrontEnd/src/views/timeline/Timeline.tsx b/FrontEnd/src/views/timeline/Timeline.tsx index e8ed9fe5..a2047651 100644 --- a/FrontEnd/src/views/timeline/Timeline.tsx +++ b/FrontEnd/src/views/timeline/Timeline.tsx @@ -89,7 +89,7 @@ const Timeline: React.FC<TimelineProps> = (props) => { ([t, p]) => { if (subscribe) { setTimeline(t); - setPosts(p); + setPosts(p.items); setState("loaded"); onTimelineLoaded.current?.(t); } diff --git a/FrontEnd/src/views/timeline/TimelineCard.tsx b/FrontEnd/src/views/timeline/TimelineCard.tsx index dcf5e870..872ad6d3 100644 --- a/FrontEnd/src/views/timeline/TimelineCard.tsx +++ b/FrontEnd/src/views/timeline/TimelineCard.tsx @@ -8,7 +8,6 @@ import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; import { useUser } from "@/services/user"; import { pushAlert } from "@/services/alert"; import { HttpTimelineInfo } from "@/http/timeline"; -import { getHttpHighlightClient } from "@/http/highlight"; import { getHttpBookmarkClient } from "@/http/bookmark"; import UserAvatar from "../common/user/UserAvatar"; @@ -71,28 +70,6 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => { {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} </small> <div className="mt-2 cru-text-end"> - <i - className={classnames( - timeline.isHighlight ? "bi-star-fill" : "bi-star", - "icon-button cru-color-primary me-3" - )} - onClick={ - user?.hasHighlightTimelineAdministrationPermission - ? () => { - getHttpHighlightClient() - [timeline.isHighlight ? "delete" : "put"](timeline.nameV2) - .then(onReload, () => { - pushAlert({ - message: timeline.isHighlight - ? "timeline.removeHighlightFail" - : "timeline.addHighlightFail", - type: "danger", - }); - }); - } - : undefined - } - /> {user != null ? ( <i className={classnames( @@ -101,7 +78,11 @@ const TimelineCard: React.FC<TimelinePageCardProps> = (props) => { )} onClick={() => { getHttpBookmarkClient() - [timeline.isBookmark ? "delete" : "put"](timeline.nameV2) + [timeline.isBookmark ? "delete" : "post"]( + user.username, + timeline.owner.username, + timeline.nameV2 + ) .then(onReload, () => { pushAlert({ message: timeline.isBookmark |