Added page and endpoints for managing Tags
This commit is contained in:
@ -39,20 +39,6 @@ namespace YABA.API.Controllers
|
||||
return CreatedAtAction(nameof(Create), _mapper.Map<BookmarkResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/Tags")]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>),(int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> UpdateBookmarkTags(int id, [FromBody] UpdateBookmarkTagRequest request)
|
||||
{
|
||||
if (request.Tags == null || !request.Tags.Any()) return BadRequest();
|
||||
|
||||
var result = await _bookmarkService.UpdateBookmarkTags(id, request.Tags);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<IEnumerable<TagResponse>>(result));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
@ -106,18 +92,6 @@ namespace YABA.API.Controllers
|
||||
return Ok(_mapper.Map<BookmarkResponse>(result));
|
||||
}
|
||||
|
||||
[HttpGet("{id}/Tags")]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public IActionResult GetBookmarkTags(int id)
|
||||
{
|
||||
var result = _bookmarkService.GetBookmarkTags(id);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<IEnumerable<TagResponse>>(result));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
@ -160,5 +134,13 @@ namespace YABA.API.Controllers
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("Tags")]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
public IActionResult GetBookmarkTags(bool showHidden = false)
|
||||
{
|
||||
var result = _bookmarkService.GetAllBookmarkTags(showHidden);
|
||||
return Ok(_mapper.Map<IEnumerable<TagResponse>>(result));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using YABA.API.ViewModels.Tags;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.API.Controllers
|
||||
@ -24,10 +25,86 @@ namespace YABA.API.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
public IActionResult GetTags()
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> GetAll()
|
||||
{
|
||||
var result = _tagsService.GetAll();
|
||||
var result = await _tagsService.GetAll();
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<IEnumerable<TagResponse>>(result));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(TagResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Get(int id)
|
||||
{
|
||||
var result = await _tagsService.Get(id);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<TagResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> CreateTag([FromBody]CreateTagDTO request)
|
||||
{
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var result = await _tagsService.CreateTag(request);
|
||||
|
||||
if (result == null) return BadRequest();
|
||||
|
||||
return Ok(_mapper.Map<TagResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> UpdateTag(int id, [FromBody]UpdateTagDTO request)
|
||||
{
|
||||
var result = await _tagsService.UpdateTag(id, request);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<TagResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> PatchTag(int id, [FromBody] UpdateTagDTO request) => await UpdateTag(id, request);
|
||||
|
||||
|
||||
[HttpDelete]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType ((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> DeleteTags([FromBody] DeleteTagsRequest request)
|
||||
{
|
||||
if(request.Ids == null || !request.Ids.Any()) return BadRequest();
|
||||
|
||||
var result = await _tagsService.DeleteTags(request.Ids);
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("Hide")]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> HideTags([FromBody] HideTagsRequest request)
|
||||
{
|
||||
if (request.Ids == null || !request.Ids.Any()) return BadRequest();
|
||||
|
||||
var result = await _tagsService.HideTags(request.Ids);
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,11 +13,11 @@ namespace YABA.API.Settings
|
||||
public AutoMapperProfile()
|
||||
{
|
||||
CreateMap<UserDTO, UserResponse>();
|
||||
CreateMap<TagSummaryDTO, TagResponse>();
|
||||
CreateMap<BookmarkDTO, BookmarkResponse>();
|
||||
CreateMap<WebsiteMetaDataDTO, GetWebsiteMetaDataResponse>();
|
||||
CreateMap<BookmarkDTO, PatchBookmarkRequest>();
|
||||
CreateMap<PatchBookmarkRequest, UpdateBookmarkRequestDTO>();
|
||||
CreateMap<TagDTO, TagResponse>().ReverseMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
YABA.API/ViewModels/Tags/DeleteTagsRequest.cs
Normal file
7
YABA.API/ViewModels/Tags/DeleteTagsRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace YABA.API.ViewModels.Tags
|
||||
{
|
||||
public class DeleteTagsRequest
|
||||
{
|
||||
public IEnumerable<int> Ids { get; set; }
|
||||
}
|
||||
}
|
||||
7
YABA.API/ViewModels/Tags/HideTagsRequest.cs
Normal file
7
YABA.API/ViewModels/Tags/HideTagsRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace YABA.API.ViewModels.Tags
|
||||
{
|
||||
public class HideTagsRequest
|
||||
{
|
||||
public IEnumerable<int> Ids { get; set; }
|
||||
}
|
||||
}
|
||||
@ -4,5 +4,6 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,6 @@ namespace YABA.Common.DTOs.Bookmarks
|
||||
public string? Note { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string Url { get; set; }
|
||||
public List<TagSummaryDTO>? Tags { get; set; } = new List<TagSummaryDTO>();
|
||||
public List<TagDTO>? Tags { get; set; } = new List<TagDTO>();
|
||||
}
|
||||
}
|
||||
|
||||
11
YABA.Common/DTOs/Tags/CreateTagDTO.cs
Normal file
11
YABA.Common/DTOs/Tags/CreateTagDTO.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YABA.Common.DTOs.Tags
|
||||
{
|
||||
public class CreateTagDTO
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@
|
||||
public class TagDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
namespace YABA.Common.DTOs.Tags
|
||||
{
|
||||
public class TagSummaryDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
10
YABA.Common/DTOs/Tags/UpdateTagDTO.cs
Normal file
10
YABA.Common/DTOs/Tags/UpdateTagDTO.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YABA.Common.DTOs.Tags
|
||||
{
|
||||
public class UpdateTagDTO
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public bool? IsHidden { get; set; }
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,7 @@ namespace YABA.Service
|
||||
{
|
||||
foreach (var bookmarkTag in bookmarkTags)
|
||||
{
|
||||
var tagDTO = _mapper.Map<TagSummaryDTO>(bookmarkTag.Tag);
|
||||
var tagDTO = _mapper.Map<TagDTO>(bookmarkTag.Tag);
|
||||
bookmarkDTO.Tags.Add(tagDTO);
|
||||
}
|
||||
}
|
||||
@ -101,7 +101,7 @@ namespace YABA.Service
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
var bookmark = _context.Bookmarks.FirstOrDefault(x => x.UserId == currentUserId && x.Id == id);
|
||||
var tags = new List<TagSummaryDTO>();
|
||||
var tags = new List<TagDTO>();
|
||||
|
||||
if (request.Tags != null && request.Tags.Any())
|
||||
tags = (await UpdateBookmarkTags(id, request.Tags)).ToList();
|
||||
@ -126,7 +126,7 @@ namespace YABA.Service
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TagSummaryDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags)
|
||||
public async Task<IEnumerable<TagDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
@ -163,7 +163,7 @@ namespace YABA.Service
|
||||
.Where(x => x.BookmarkId == id)
|
||||
.Select(x => x.Tag);
|
||||
|
||||
return _mapper.Map<IEnumerable<TagSummaryDTO>>(updatedBookmarkTags);
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(updatedBookmarkTags);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -183,12 +183,12 @@ namespace YABA.Service
|
||||
.ToList();
|
||||
|
||||
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
||||
bookmarkDTO.Tags = _mapper.Map<List<TagSummaryDTO>>(bookmarkTags);
|
||||
bookmarkDTO.Tags = _mapper.Map<List<TagDTO>>(bookmarkTags);
|
||||
|
||||
return bookmarkDTO;
|
||||
}
|
||||
|
||||
public IEnumerable<TagSummaryDTO>? GetBookmarkTags(int id)
|
||||
public IEnumerable<TagDTO>? GetBookmarkTags(int id)
|
||||
{
|
||||
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
||||
if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == userId)) return null;
|
||||
@ -199,7 +199,7 @@ namespace YABA.Service
|
||||
.Select(x => x.Tag)
|
||||
.ToList();
|
||||
|
||||
return _mapper.Map<IEnumerable<TagSummaryDTO>>(bookmarkTags);
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(bookmarkTags);
|
||||
}
|
||||
|
||||
public async Task<int?> DeleteBookmark(int id)
|
||||
@ -236,6 +236,20 @@ namespace YABA.Service
|
||||
return entriesToHide.Select(x => x.Id);
|
||||
}
|
||||
|
||||
public IEnumerable<TagDTO> GetAllBookmarkTags(bool showHidden = false)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
var activeUserTags = _roContext.BookmarkTags
|
||||
.Include(x => x.Tag)
|
||||
.Where(x => x.Tag.UserId == currentUserId && x.Tag.IsHidden == showHidden)
|
||||
.ToList()
|
||||
.GroupBy(x => x.Tag.Id)
|
||||
.Select(g => g.First()?.Tag);
|
||||
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(activeUserTags);
|
||||
}
|
||||
|
||||
private int GetCurrentUserId()
|
||||
{
|
||||
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
||||
|
||||
@ -15,7 +15,8 @@ namespace YABA.Service.Configuration
|
||||
CreateMap<Bookmark, BookmarkDTO>();
|
||||
CreateMap<CreateBookmarkRequestDTO, Bookmark>();
|
||||
CreateMap<Tag, TagDTO>();
|
||||
CreateMap<Tag, TagSummaryDTO>();
|
||||
CreateMap<CreateTagDTO, Tag>()
|
||||
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name.ToLowerInvariant()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs;
|
||||
using YABA.Common.DTOs.Bookmarks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
|
||||
@ -10,13 +9,14 @@ namespace YABA.Service.Interfaces
|
||||
{
|
||||
Task<BookmarkDTO?> CreateBookmark(CreateBookmarkRequestDTO request);
|
||||
Task<BookmarkDTO?> UpdateBookmark(int id, UpdateBookmarkRequestDTO request);
|
||||
Task<IEnumerable<TagSummaryDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags);
|
||||
Task<IEnumerable<TagDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags);
|
||||
IEnumerable<BookmarkDTO> GetAll(bool isHidden = false);
|
||||
Task<BookmarkDTO?> Get(int id);
|
||||
IEnumerable<TagSummaryDTO>? GetBookmarkTags(int id);
|
||||
IEnumerable<TagDTO>? GetBookmarkTags(int id);
|
||||
Task<int?> DeleteBookmark(int id);
|
||||
Task<IEnumerable<int>?> DeleteBookmarks(IEnumerable<int> ids);
|
||||
Task<IEnumerable<int>?> HideBookmarks(IEnumerable<int> ids);
|
||||
IEnumerable<TagDTO> GetAllBookmarkTags(bool showHidden = false);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
|
||||
namespace YABA.Service.Interfaces
|
||||
{
|
||||
public interface ITagsService
|
||||
{
|
||||
public IEnumerable<TagSummaryDTO> GetAll();
|
||||
Task<IEnumerable<TagDTO>?> GetAll();
|
||||
Task<TagDTO?> Get(int id);
|
||||
Task<IEnumerable<TagDTO>?> UpsertTags(IEnumerable<TagDTO> tags);
|
||||
Task<TagDTO?> UpsertTag(TagDTO tag);
|
||||
Task<IEnumerable<int>?> DeleteTags(IEnumerable<int> ids);
|
||||
Task<IEnumerable<int>?> HideTags(IEnumerable<int> ids);
|
||||
Task<TagDTO?> CreateTag(CreateTagDTO request);
|
||||
Task<TagDTO?> UpdateTag(int id, UpdateTagDTO request);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
using YABA.Common.Extensions;
|
||||
using YABA.Data.Context;
|
||||
using YABA.Data.Extensions;
|
||||
using YABA.Models;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.Service
|
||||
@ -13,29 +17,111 @@ namespace YABA.Service
|
||||
public class TagsService : ITagsService
|
||||
{
|
||||
private readonly YABAReadOnlyContext _roContext;
|
||||
private readonly YABAReadWriteContext _rwContext;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public TagsService(YABAReadOnlyContext roContext, IHttpContextAccessor httpContextAccessor, IMapper mapper)
|
||||
public TagsService(
|
||||
YABAReadOnlyContext roContext,
|
||||
YABAReadWriteContext rwContext,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IMapper mapper)
|
||||
{
|
||||
_roContext = roContext;
|
||||
_rwContext = rwContext;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public IEnumerable<TagSummaryDTO> GetAll()
|
||||
public async Task<IEnumerable<TagDTO>?> GetAll()
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var activeUserTags = _roContext.Tags
|
||||
.Where(x => x.UserId == currentUserId)
|
||||
.ToList();
|
||||
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(activeUserTags);
|
||||
}
|
||||
|
||||
public async Task<TagDTO?> Get(int id)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var bookmark = await _roContext.Tags.FirstOrDefaultAsync(x => x.Id == id && x.UserId == currentUserId);
|
||||
return _mapper.Map<TagDTO>(bookmark);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TagDTO>?> UpsertTags(IEnumerable<TagDTO> tags)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var tagsToUpsert = tags.Select(x => new Tag { Id = x.Id, Name = x.Name.ToLower(), IsHidden = x.IsHidden, UserId = currentUserId});
|
||||
_rwContext.Tags.UpsertRange(tagsToUpsert);
|
||||
await _rwContext.SaveChangesAsync();
|
||||
|
||||
var newlyUpsertedTags = _roContext.Tags.Where(x => tagsToUpsert.Select(x => x.Name).Contains(x.Name)).ToList();
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(newlyUpsertedTags);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>?> DeleteTags(IEnumerable<int> ids)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var entriesToDelete = _rwContext.Tags.Where(x => x.UserId == currentUserId && ids.Contains(x.Id)).ToList();
|
||||
_rwContext.Tags.RemoveRange(entriesToDelete);
|
||||
|
||||
if (await _rwContext.SaveChangesAsync() <= 0) return null;
|
||||
|
||||
return entriesToDelete.Select(x => x.Id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>?> HideTags(IEnumerable<int> ids)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var entriesToHide = _rwContext.Tags.Where(x => x.UserId == currentUserId && ids.Contains(x.Id)).ToList();
|
||||
entriesToHide.ForEach((x) => { x.IsHidden = !x.IsHidden; });
|
||||
|
||||
if (await _rwContext.SaveChangesAsync() <= 0) return null;
|
||||
|
||||
return entriesToHide.Select(x => x.Id);
|
||||
}
|
||||
|
||||
public async Task<TagDTO?> UpsertTag(TagDTO tag) => (await UpsertTags(new List<TagDTO>() { tag }))?.FirstOrDefault();
|
||||
|
||||
public async Task<TagDTO?> CreateTag(CreateTagDTO request)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
var activeUserTags = _roContext.BookmarkTags
|
||||
.Include(x => x.Tag)
|
||||
.Where(x => x.Tag.UserId == currentUserId)
|
||||
.ToList()
|
||||
.GroupBy(x => x.Tag.Id)
|
||||
.Select(g => g.First()?.Tag);
|
||||
if(!(await _roContext.Users.UserExistsAsync(currentUserId))
|
||||
|| await _roContext.Tags.AnyAsync(x => x.UserId == currentUserId && x.Name.ToLower() == request.Name.ToLower()))
|
||||
return null;
|
||||
|
||||
return _mapper.Map<IEnumerable<TagSummaryDTO>>(activeUserTags);
|
||||
var tag = _mapper.Map<Tag>(request);
|
||||
tag.UserId = currentUserId;
|
||||
|
||||
var newEntity = await _rwContext.Tags.AddAsync(tag);
|
||||
|
||||
return await _rwContext.SaveChangesAsync() > 0 ? _mapper.Map<TagDTO>(newEntity.Entity) : null;
|
||||
}
|
||||
|
||||
public async Task<TagDTO?> UpdateTag(int id, UpdateTagDTO request)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
var tag = await _rwContext.Tags.FirstOrDefaultAsync(x => x.UserId == currentUserId && x.Id == id);
|
||||
|
||||
if (tag == null) return null;
|
||||
|
||||
tag.Name = !string.IsNullOrEmpty(request.Name) ? request.Name.ToLower() : tag.Name;
|
||||
tag.IsHidden = request.IsHidden.HasValue ? request.IsHidden.Value : tag.IsHidden;
|
||||
|
||||
return await _rwContext.SaveChangesAsync() > 0 ? _mapper.Map<TagDTO>(tag) : null;
|
||||
}
|
||||
|
||||
private int GetCurrentUserId()
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
import React from "react";
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { Footer, Header, ProtectedRoute } from './components';
|
||||
import { BaseLayout, HomeView, RedirectView, BookmarksListView, BookmarkDetailView, TestView, NotFoundView } from './views';
|
||||
import { BaseLayout, HomeView, RedirectView, BookmarksListView, BookmarkDetailView, TestView, NotFoundView, TagsView } from './views';
|
||||
import { isDev } from "./utils";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App" style={{minHeight: '100vh', display: 'flex', flexDirection: 'column'}}>
|
||||
<Routes>
|
||||
|
||||
<Route
|
||||
path="/"
|
||||
element={<BaseLayout header={ <Header />} content={ <HomeView /> } footer={ <Footer />}/>}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/redirect"
|
||||
element={<BaseLayout header={ <Header />} content={ <RedirectView />} footer={ <Footer />}/> }
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/bookmarks"
|
||||
element={
|
||||
@ -29,6 +30,7 @@ function App() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/bookmarks/hidden"
|
||||
element={
|
||||
@ -41,6 +43,7 @@ function App() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/bookmarks/new"
|
||||
element={
|
||||
@ -52,6 +55,7 @@ function App() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/bookmarks/:id"
|
||||
element={
|
||||
@ -63,16 +67,30 @@ function App() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/404"
|
||||
element={<BaseLayout content={<NotFoundView />} />}
|
||||
/>
|
||||
|
||||
{ isDev() && (
|
||||
<Route
|
||||
path="/test"
|
||||
element={<BaseLayout header={ <Header />} content={ <TestView /> } footer={ <Footer />}/>}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Route
|
||||
path="/tags"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
layout={BaseLayout}
|
||||
header={Header}
|
||||
footer={Footer}
|
||||
view={TagsView}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { callExternalApi } from "./apiHelper";
|
||||
import { getAllBookmarks, getBookmark, createNewBookmark, updateBookmark, deleteBookmark, deleteBookmarks, hideBookmarks } from "./v1/bookmarks";
|
||||
import { getAllTags } from "./v1/tags";
|
||||
import { getAllTags, createNewTag, updateTag, deleteTags, hideTags } from "./v1/tags";
|
||||
import { getWebsiteMetaData } from "./v1/misc";
|
||||
|
||||
export {
|
||||
@ -10,8 +10,12 @@ export {
|
||||
createNewBookmark,
|
||||
updateBookmark,
|
||||
getWebsiteMetaData,
|
||||
getAllTags,
|
||||
deleteBookmark,
|
||||
deleteBookmarks,
|
||||
hideBookmarks,
|
||||
getAllTags,
|
||||
createNewTag,
|
||||
updateTag,
|
||||
deleteTags,
|
||||
hideTags,
|
||||
};
|
||||
@ -13,4 +13,64 @@ export const getAllTags = async(accessToken) => {
|
||||
};
|
||||
|
||||
return await callExternalApi({config});
|
||||
};
|
||||
|
||||
export const createNewTag = async(accessToken, newTag) => {
|
||||
const config = {
|
||||
url: `${apiServerUrl}`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
data: newTag
|
||||
};
|
||||
|
||||
return await callExternalApi({config});
|
||||
};
|
||||
|
||||
export const updateTag = async(accessToken, id, updatedTagEntry) => {
|
||||
const config = {
|
||||
url: `${apiServerUrl}/${id}`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
data: updatedTagEntry
|
||||
};
|
||||
|
||||
return await callExternalApi({config});
|
||||
};
|
||||
|
||||
export const deleteTags = async(accessToken, ids) => {
|
||||
const config = {
|
||||
url: `${apiServerUrl}`,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
data: {
|
||||
ids: ids
|
||||
}
|
||||
};
|
||||
|
||||
return await callExternalApi({config});
|
||||
};
|
||||
|
||||
export const hideTags = async(accessToken, ids) => {
|
||||
const config = {
|
||||
url: `${apiServerUrl}/Hide`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
data: {
|
||||
ids: ids
|
||||
}
|
||||
};
|
||||
|
||||
return await callExternalApi({config});
|
||||
};
|
||||
6
yaba-web/src/components/external/index.js
vendored
6
yaba-web/src/components/external/index.js
vendored
@ -5,6 +5,9 @@ import Row from "react-bootstrap/Row";
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Modal from 'react-bootstrap/Modal'
|
||||
import { Table } from "react-bootstrap";
|
||||
import Dropdown from 'react-bootstrap/Dropdown';
|
||||
import DropdownButton from 'react-bootstrap/DropdownButton';
|
||||
|
||||
export {
|
||||
Alert,
|
||||
@ -14,4 +17,7 @@ export {
|
||||
Form,
|
||||
Button,
|
||||
Modal,
|
||||
Table,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
};
|
||||
@ -57,6 +57,7 @@ export function Header(props) {
|
||||
<NavDropdown.Divider />
|
||||
<NavDropdown.Item href="/bookmarks/new">New</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
<Nav.Link href="/tags">Tags</Nav.Link>
|
||||
<Nav.Link onClick={handleLogout}>Logout</Nav.Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -5,6 +5,8 @@ import { Auth0ProviderWithNavigate } from "./auth0ProviderWithNavigate";
|
||||
import { ProtectedRoute } from "./protectedRoute";
|
||||
import { SplashScreen } from "./shared/splashScreen";
|
||||
import { SearchForm } from "./shared/searchForm";
|
||||
import { UpsertTagModal } from "./tags/upsertTagModal";
|
||||
import { InterceptModal } from "./shared/interceptModal";
|
||||
|
||||
export {
|
||||
Footer,
|
||||
@ -13,5 +15,7 @@ export {
|
||||
Auth0ProviderWithNavigate,
|
||||
ProtectedRoute,
|
||||
SplashScreen,
|
||||
SearchForm
|
||||
SearchForm,
|
||||
UpsertTagModal,
|
||||
InterceptModal,
|
||||
};
|
||||
32
yaba-web/src/components/shared/interceptModal.jsx
Normal file
32
yaba-web/src/components/shared/interceptModal.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { Button, Form, Modal } from "../external";
|
||||
|
||||
export function InterceptModal(props) {
|
||||
return (
|
||||
<Modal
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
keyboard={false}
|
||||
backdrop={props.backdrop}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>{props.message}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant={props.secondaryAction.variant}
|
||||
onClick={props.secondaryAction.onClick}
|
||||
>
|
||||
{props.secondaryAction.text}
|
||||
</Button>
|
||||
<Button
|
||||
variant={props.primaryAction.variant}
|
||||
onClick={props.primaryAction.onClick}
|
||||
>
|
||||
{props.primaryAction.text}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
73
yaba-web/src/components/tags/upsertTagModal.jsx
Normal file
73
yaba-web/src/components/tags/upsertTagModal.jsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import { Button, Form, Modal } from "../external";
|
||||
import { useFormik } from "formik";
|
||||
import * as Yup from "yup"
|
||||
|
||||
export function UpsertTagModal(props) {
|
||||
const formik = useFormik({
|
||||
initialValues: props.tag,
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required("Name is required").lowercase()
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
onSubmit: async(values) => props.onSave(values)
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
keyboard={false}
|
||||
backdrop="static"
|
||||
>
|
||||
<Form onSubmit={formik.handleSubmit}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.tag.id > 0 ? "Edit Tag" : "New Tag"}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Name:</Form.Label>
|
||||
<Form.Control
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Enter tag name"
|
||||
defaultValue={formik.values.name}
|
||||
onBlur={formik.handleChange}
|
||||
isInvalid={!!formik.errors.name}
|
||||
/>
|
||||
<Form.Control.Feedback
|
||||
type="invalid"
|
||||
className="d-flex justify-content-start"
|
||||
>
|
||||
{formik.errors.name}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="isHidden"
|
||||
type="switch"
|
||||
label="Mark as hidden"
|
||||
checked={formik.values.isHidden}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{ props.tag.id > 0 && <Button
|
||||
variant="danger"
|
||||
onClick={props.onDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useReducer, useState } from "react";
|
||||
import { Alert, Col, Container, Row, Form, Button, Modal } from "../components/external";
|
||||
import { Alert, Col, Container, Row, Button, Modal } from "../components/external";
|
||||
import { Bookmark } from "../components";
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
import { deleteBookmarks, getAllBookmarks, getAllTags, hideBookmarks } from "../api";
|
||||
|
||||
@ -5,6 +5,7 @@ import { BookmarksListView } from "./bookmarksListView";
|
||||
import { BookmarkDetailView } from "./bookmarkDetailView";
|
||||
import { TestView } from "./testView";
|
||||
import { NotFoundView } from "./notFoundView";
|
||||
import { TagsView } from "./tagsView";
|
||||
|
||||
export {
|
||||
BaseLayout,
|
||||
@ -13,5 +14,6 @@ export {
|
||||
BookmarksListView,
|
||||
BookmarkDetailView,
|
||||
TestView,
|
||||
NotFoundView
|
||||
}
|
||||
NotFoundView,
|
||||
TagsView,
|
||||
};
|
||||
432
yaba-web/src/views/tagsView.jsx
Normal file
432
yaba-web/src/views/tagsView.jsx
Normal file
@ -0,0 +1,432 @@
|
||||
import React, { useEffect, useReducer, useState } from "react";
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
import { containsSubstring } from "../utils";
|
||||
import { SplashScreen, SearchForm, UpsertTagModal, InterceptModal } from "../components";
|
||||
import { Alert, Button, Col, Container, Dropdown, DropdownButton, Form, Row, Table } from "../components/external";
|
||||
import { getAllTags, createNewTag, updateTag, deleteTags, hideTags } from "../api";
|
||||
|
||||
export function TagsView(props) {
|
||||
const { getAccessTokenSilently } = useAuth0();
|
||||
|
||||
const [searchString, setSearchString] = useState("");
|
||||
const handleSearch = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if(!searchString) {
|
||||
dispatchTagsState({type: "DISPLAY_ALL"});
|
||||
} else {
|
||||
dispatchTagsState({type: "SEARCH"});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchStringChange = (e) => {
|
||||
if(!e.target.value) {
|
||||
dispatchTagsState({type: "DISPLAY_ALL"});
|
||||
} else {
|
||||
setSearchString(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const [isAllTagsSelected, setAllTagsSelected] = useState(false);
|
||||
|
||||
const initialSplashScreenState = { show: false, message: null };
|
||||
const splashScreenReducer = (state = initialSplashScreenState, action) => {
|
||||
switch(action.type) {
|
||||
case "SHOW_SPLASH_SCREEN":
|
||||
return Object.assign({}, state, {
|
||||
show: true,
|
||||
message: action.payload.message
|
||||
});
|
||||
case "HIDE_SPLASH_SCREEN":
|
||||
default:
|
||||
return Object.assign({}, state, {
|
||||
show: false,
|
||||
message: null
|
||||
});
|
||||
}
|
||||
};
|
||||
const [splashScreenState, dispatchSplashScreenState] = useReducer(splashScreenReducer, initialSplashScreenState);
|
||||
|
||||
const alertMessageInitialState = { show: false, type: "primary", message: null};
|
||||
const alertReducer = (state = alertMessageInitialState, action) => {
|
||||
switch(action.type) {
|
||||
case "SHOW_ALERT":
|
||||
return Object.assign({}, state, {
|
||||
show: action.payload.show,
|
||||
type: action.payload.alertType,
|
||||
message: action.payload.message
|
||||
});
|
||||
case "UPSERT_FAIL":
|
||||
return {
|
||||
show: true,
|
||||
type: "danger",
|
||||
message: action.payload.message
|
||||
};
|
||||
case "UPSERT_SUCCEEDED":
|
||||
return {
|
||||
show: true,
|
||||
type: "success",
|
||||
message: action.payload.message
|
||||
};
|
||||
case "NO_TAG_TO_DELETE":
|
||||
return {
|
||||
show: true,
|
||||
type: "warning",
|
||||
message: "No tag(s) to delete"
|
||||
};
|
||||
case "DELETE_FAILED":
|
||||
return {
|
||||
show: true,
|
||||
type: "danger",
|
||||
message: action.payload.message
|
||||
};
|
||||
case "DELETE_SUCCEEDED":
|
||||
return {
|
||||
show: true,
|
||||
type: "success",
|
||||
message: "Tag(s) deleted"
|
||||
}
|
||||
case "NO_TAG_TO_HIDE":
|
||||
return {
|
||||
show: true,
|
||||
type: "warning",
|
||||
message: "No tag(s) to hide/unhide"
|
||||
}
|
||||
case "HIDE_ALERT":
|
||||
default:
|
||||
return Object.assign({}, state, {
|
||||
show: false
|
||||
});
|
||||
}
|
||||
};
|
||||
const [alertMessageState, dispatchAlertMessageState] = useReducer(alertReducer, alertMessageInitialState);
|
||||
|
||||
const tagsReducer = (state = [], action) => {
|
||||
let newState = [];
|
||||
|
||||
switch(action.type) {
|
||||
case "SET":
|
||||
newState = action.payload.tags.map(x => ({...x, isSelected: false, isDisplayed: true}));
|
||||
break;
|
||||
case "ADD_SELECTED":
|
||||
case "REMOVE_SELECTED":
|
||||
newState = [...state];
|
||||
const selectedTagIndex = newState.findIndex(x => x.id === action.payload.selectedTagId);
|
||||
newState[selectedTagIndex].isSelected = action.type === "ADD_SELECTED";
|
||||
break;
|
||||
case "DELETE_SELECTED":
|
||||
newState = state.filter((x) => !action.payload.selectedTagIds.includes(x.id));
|
||||
break;
|
||||
case "SELECT_ALL":
|
||||
newState = state.filter(x => x.isDisplayed).map(x => ({...x, isSelected: true}));
|
||||
break;
|
||||
case "UNSELECT_ALL":
|
||||
newState = state.filter(x => x.isDisplayed).map(x => ({...x, isSelected: false}));
|
||||
break;
|
||||
case "DISPLAY_ALL":
|
||||
newState = state.map(x => ({...x, isDisplayed: true}));
|
||||
break;
|
||||
case "SEARCH":
|
||||
if(!searchString) {
|
||||
dispatchTagsState({type: "DISPLAY_ALL"});
|
||||
}
|
||||
|
||||
newState = state.map(x => ({...x, isDisplayed: containsSubstring(x, searchString)}));
|
||||
break;
|
||||
case "TAG_UPDATE":
|
||||
newState = [...state];
|
||||
const updatedTagIndex = newState.findIndex(x => x.id == action.payload.tag.id);
|
||||
newState.splice(updatedTagIndex, 1, Object.assign({}, newState[updatedTagIndex], action.payload.tag));
|
||||
break;
|
||||
case "TAG_INSERT":
|
||||
newState = [{...action.payload.tag, isDisplayed: true, isSelected: false}, ...state];
|
||||
break;
|
||||
case "HIDE_SUCCEEDED":
|
||||
newState = [...state.filter(x => !x.isSelected), ...state.filter(x => x.isSelected).map(x => Object.assign({}, x, {isHidden: !x.isHidden}))];
|
||||
break;
|
||||
default:
|
||||
newState = state;
|
||||
break;
|
||||
}
|
||||
|
||||
setAllTagsSelected(newState.every(x => x.isSelected));
|
||||
return newState;
|
||||
};
|
||||
const [tagsState, dispatchTagsState] = useReducer(tagsReducer, []);
|
||||
const getTagsSelectedCount = () => tagsState.filter(x => x.isSelected).length;
|
||||
const onTagSelected = (isTagSelected, tag) => dispatchTagsState({type: isTagSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedTagId: tag.id}});
|
||||
const getDisplayedTags = () => tagsState.filter(tag => tag.isDisplayed);
|
||||
const getSelectedTags = () => tagsState.filter(tag => tag.isSelected);
|
||||
const isAllSelectedHidden = () => getSelectedTags().every(tag => tag.isHidden);
|
||||
const isAllSelectedNotHidden = () => getSelectedTags().every(tag => !tag.isHidden);
|
||||
|
||||
const upsertTagModalInitialState = { show: false, tag: { id: 0, name: "", isHidden: false }};
|
||||
const upsertTagModalReducer = (state = upsertTagModalInitialState, action) => {
|
||||
switch(action.type) {
|
||||
case "SHOW_EDIT":
|
||||
return {show: true, tag: action.payload.tag};
|
||||
case "SHOW_NEW":
|
||||
return {...upsertTagModalInitialState, show: true};
|
||||
case "HIDE":
|
||||
default:
|
||||
return upsertTagModalInitialState;
|
||||
}
|
||||
}
|
||||
const [upsertTagModalState, dispatchUpsertTagModal] = useReducer(upsertTagModalReducer, upsertTagModalInitialState);
|
||||
|
||||
const initialInterceptModalState = {
|
||||
show: false,
|
||||
backdrop: "static",
|
||||
title: null,
|
||||
message: null,
|
||||
secondaryAction: {
|
||||
variant: "secondary",
|
||||
text: "Cancel",
|
||||
onClick: null,
|
||||
},
|
||||
primaryAction: {
|
||||
variant: "danger",
|
||||
text: "Delete",
|
||||
onClick: null,
|
||||
},
|
||||
selectedTagIds: []
|
||||
};
|
||||
const interceptModalReducer = (state = initialInterceptModalState, action) => {
|
||||
switch(action.type) {
|
||||
case "SHOW_FOR_MULTIPLE_DELETE":
|
||||
return {
|
||||
...state,
|
||||
show: true,
|
||||
title: "Delete Tags",
|
||||
message: "Are you sure you want to delete selected tags?",
|
||||
secondaryAction: {
|
||||
variant: "secondary",
|
||||
text: "Cancel",
|
||||
onClick: null,
|
||||
},
|
||||
primaryAction: {
|
||||
variant: "danger",
|
||||
text: "Delete",
|
||||
onClick: null,
|
||||
},
|
||||
selectedTagIds: action.payload.selectedTagIds,
|
||||
};
|
||||
case "SHOW_FOR_DELETE":
|
||||
return {
|
||||
...state,
|
||||
show: true,
|
||||
title: "Delete Tag",
|
||||
message: "Are you sure you want to delete this tag?",
|
||||
secondaryAction: {
|
||||
variant: "secondary",
|
||||
text: "Cancel",
|
||||
onClick: null,
|
||||
},
|
||||
primaryAction: {
|
||||
variant: "danger",
|
||||
text: "Delete",
|
||||
onClick: null,
|
||||
},
|
||||
selectedTagIds: action.payload.selectedTagIds,
|
||||
};
|
||||
case "HIDE":
|
||||
default:
|
||||
return initialInterceptModalState;
|
||||
}
|
||||
};
|
||||
const [interceptModalState, dispatchInterceptModalState] = useReducer(interceptModalReducer, initialInterceptModalState);
|
||||
|
||||
const fetchTags = async() => {
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
const { data, error } = await getAllTags(accessToken);
|
||||
|
||||
if(error) {
|
||||
dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "danger", "message": `Error fetching tags: ${error.message}`}});
|
||||
} else {
|
||||
dispatchTagsState({type: "SET", payload: {tags: data}});
|
||||
}
|
||||
dispatchSplashScreenState({type: "HIDE_SPLASH_SCREEN", payload: {message: null}});
|
||||
};
|
||||
|
||||
const handleUpsertTag = async(tagEntry) => {
|
||||
const isUpdate = tagEntry.id > 0;
|
||||
dispatchAlertMessageState({ type: "CLOSE_ALERT"});
|
||||
dispatchUpsertTagModal({type: "HIDE"});
|
||||
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: isUpdate ? "Updating Bookmark entry" : "Creating new Bookmark"}});
|
||||
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
const { data, error } = isUpdate ?
|
||||
await updateTag(accessToken, tagEntry.id, tagEntry) :
|
||||
await createNewTag(accessToken, tagEntry);
|
||||
|
||||
if(error) {
|
||||
dispatchAlertMessageState({type: "UPSERT_FAIL", payload: {message: `Error ${isUpdate ? "updating": "inserting new"} Tag record: ${error.message}`}});
|
||||
} else {
|
||||
dispatchAlertMessageState({type: "UPSERT_SUCCEEDED", payload: {message: isUpdate ? "Tag updated" : "Tag created"}});
|
||||
dispatchTagsState({ type: isUpdate? "TAG_UPDATE" : "TAG_INSERT", payload: {tag: data}});
|
||||
}
|
||||
|
||||
dispatchSplashScreenState({type: "HIDE_SPLASH_SCREEN", payload: {message: null}});
|
||||
};
|
||||
|
||||
const handleDeleteTags = async () => {
|
||||
dispatchInterceptModalState({type: "HIDE"});
|
||||
dispatchUpsertTagModal({type: "HIDE"})
|
||||
const tagIdsToDelete = interceptModalState.selectedTagIds;
|
||||
|
||||
if(tagIdsToDelete.length <= 0) {
|
||||
dispatchAlertMessageState({type: "NO_TAG_TO_DELETE"})
|
||||
} else {
|
||||
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Deleting Tag(s)"}});
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
const { data, error } = await deleteTags(accessToken, tagIdsToDelete);
|
||||
|
||||
if(error) {
|
||||
dispatchAlertMessageState({type: "DELETE_FAILED", payload: {message: `Error deleting tag(s): ${error.message}`}});
|
||||
} else {
|
||||
dispatchAlertMessageState({type: "DELETE_SUCCEEDED"});
|
||||
dispatchTagsState({type: "DELETE_SELECTED", payload: {selectedTagIds: tagIdsToDelete}});
|
||||
}
|
||||
|
||||
dispatchSplashScreenState({TYPE: "HIDE_SPLASH_SCREEN", payload: {message: null}});
|
||||
}
|
||||
};
|
||||
|
||||
const handleHidingTags = async () => {
|
||||
const tagIdsToHide = getSelectedTags().map(x => x.id);
|
||||
|
||||
if(tagIdsToHide.length <= 0) {
|
||||
dispatchAlertMessageState({type: "NO_TAG_TO_HIDE"})
|
||||
} else {
|
||||
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Hiding/Unhiding Tag(s)"}});
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
const { data, error } = await hideTags(accessToken, tagIdsToHide);
|
||||
|
||||
if(error) {
|
||||
dispatchAlertMessageState({type: "UPSERT_FAILED", payload: {message: `Error hiding/unhiding tag(s): ${error.message}`}});
|
||||
} else {
|
||||
dispatchAlertMessageState({type: "UPSERT_SUCCEEDED", payload: {message: `Tag(s) have been hidden/unhidden`}});
|
||||
dispatchTagsState({type: "HIDE_SUCCEEDED"});
|
||||
dispatchTagsState({type: "UNSELECT_ALL"});
|
||||
}
|
||||
|
||||
dispatchSplashScreenState({TYPE: "HIDE_SPLASH_SCREEN", payload: {message: null}});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Retrieving Tags..."}});
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{splashScreenState.show && (
|
||||
<SplashScreen
|
||||
message={splashScreenState.message}
|
||||
/>
|
||||
)}
|
||||
|
||||
<UpsertTagModal
|
||||
show={upsertTagModalState.show}
|
||||
tag={upsertTagModalState.tag}
|
||||
onCancel={() => dispatchUpsertTagModal({type: "HIDE"})}
|
||||
onHide={() => dispatchUpsertTagModal({type: "HIDE"})}
|
||||
onSave={handleUpsertTag}
|
||||
onDelete={() => dispatchInterceptModalState({type: "SHOW_FOR_DELETE", payload: {selectedTagIds: [upsertTagModalState.tag.id]}})}
|
||||
/>
|
||||
<InterceptModal
|
||||
show={interceptModalState.show}
|
||||
onHide={() => dispatchInterceptModalState({type: "HIDE"})}
|
||||
backdrop={interceptModalState.backdrop}
|
||||
title={interceptModalState.title}
|
||||
message={interceptModalState.message}
|
||||
secondaryAction={{...interceptModalState.secondaryAction, onClick: () => dispatchInterceptModalState({type: "HIDE"})}}
|
||||
primaryAction={{...interceptModalState.primaryAction, onClick: () => handleDeleteTags()}}
|
||||
/>
|
||||
|
||||
<div style={{flexGrow: 1}}>
|
||||
<Container>
|
||||
{ alertMessageState.show && <Alert
|
||||
variant={alertMessageState.type}
|
||||
onClose={() => dispatchAlertMessageState({type: "CLOSE"})}
|
||||
dismissible
|
||||
>
|
||||
{alertMessageState.message}
|
||||
</Alert>
|
||||
}
|
||||
{ tagsState.length <= 0 && !splashScreenState.show && <div className="text-center fs-1">No Tags found</div> }
|
||||
{
|
||||
tagsState.length > 0 && <>
|
||||
<Row>
|
||||
<Col xs="5">
|
||||
<SearchForm
|
||||
onHandleSearch={handleSearch}
|
||||
onSearchStringChange={handleSearchStringChange}
|
||||
onBlur={handleSearchStringChange}
|
||||
searchString={searchString}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs="4">
|
||||
<div className="d-flex justify-content-end align-items-center">
|
||||
<Button variant="primary" className="me-2" onClick={() => dispatchUpsertTagModal({type: "SHOW_NEW"})}>New</Button>
|
||||
{ getTagsSelectedCount() > 0 && <DropdownButton variant="secondary" title={`${getTagsSelectedCount()} selected`}>
|
||||
{ isAllSelectedHidden() && <Dropdown.Item onClick={handleHidingTags}>Mark as Not Hidden</Dropdown.Item>}
|
||||
{ isAllSelectedNotHidden() && <Dropdown.Item onClick={handleHidingTags}>Mark as Hidden</Dropdown.Item>}
|
||||
<Dropdown.Item onClick={() => dispatchInterceptModalState({type: "SHOW_FOR_MULTIPLE_DELETE", payload: {selectedTagIds: getSelectedTags().map(x => x.id)}})}>
|
||||
<span className="text-danger">Delete</span>
|
||||
</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="9">
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
onChange={(e) => dispatchTagsState({type: e.target.checked ? "SELECT_ALL" : "UNSELECT_ALL"})}
|
||||
checked={isAllTagsSelected}
|
||||
/>
|
||||
</th>
|
||||
<th>Tag</th>
|
||||
<th>Is Hidden?</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getDisplayedTags().map(tag => {
|
||||
return <tr key={tag.id}>
|
||||
<td>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
onChange={(e) => onTagSelected(e.target.checked, tag)}
|
||||
checked={tag.isSelected}
|
||||
/>
|
||||
</td>
|
||||
<td>{tag.name}</td>
|
||||
<td>
|
||||
<span className={tag.isHidden ? "text-danger" : "text-dark"}>{ tag.isHidden ? "Yes" : "No" }</span>
|
||||
</td>
|
||||
<td>
|
||||
<Button variant="link" className="py-0" onClick={() => dispatchUpsertTagModal({type: "SHOW_EDIT", payload: {tag: tag}})}>Edit</Button>
|
||||
</td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
}
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
};
|
||||
Reference in New Issue
Block a user