aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests2/HttpClientTestExtensions.cs15
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest.cs105
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest2.cs138
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests2/TimelineBookmarkTest3.cs192
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests2/TimelinePostTest.cs (renamed from BackEnd/Timeline.Tests/IntegratedTests2/TimelinePostTest1.cs)4
-rw-r--r--BackEnd/Timeline/Controllers/TimelineBookmark1Controller.cs76
-rw-r--r--BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs176
-rw-r--r--BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs1
-rw-r--r--BackEnd/Timeline/Models/Http/HttpTimelineBookmarkMoveRequest.cs21
-rw-r--r--BackEnd/Timeline/Models/Http/HttpTimelineBookmarkVisibility.cs12
-rw-r--r--BackEnd/Timeline/Models/Http/HttpTimelinebookmarkDeleteRequest.cs18
-rw-r--r--BackEnd/Timeline/Services/Api/TimelineBookmarkService1.cs2
-rw-r--r--FrontEnd/package.json1
-rw-r--r--FrontEnd/src/common.ts2
-rw-r--r--FrontEnd/src/http/bookmark.ts95
-rw-r--r--FrontEnd/src/http/common.ts8
-rw-r--r--FrontEnd/src/http/highlight.ts49
-rw-r--r--FrontEnd/src/http/timeline.ts7
-rw-r--r--FrontEnd/src/views/center/CenterBoards.tsx61
-rw-r--r--FrontEnd/src/views/center/TimelineBoard.tsx55
-rw-r--r--FrontEnd/src/views/home/TimelineListView.tsx24
-rw-r--r--FrontEnd/src/views/home/index.tsx14
-rw-r--r--FrontEnd/src/views/timeline/Timeline.tsx2
-rw-r--r--FrontEnd/src/views/timeline/TimelineCard.tsx29
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