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));
|
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}")]
|
[HttpPut("{id}")]
|
||||||
[ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.OK)]
|
[ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.OK)]
|
||||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||||
@ -106,18 +92,6 @@ namespace YABA.API.Controllers
|
|||||||
return Ok(_mapper.Map<BookmarkResponse>(result));
|
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}")]
|
[HttpDelete("{id}")]
|
||||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||||
@ -160,5 +134,13 @@ namespace YABA.API.Controllers
|
|||||||
return NoContent();
|
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 Microsoft.AspNetCore.Mvc;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using YABA.API.ViewModels.Tags;
|
using YABA.API.ViewModels.Tags;
|
||||||
|
using YABA.Common.DTOs.Tags;
|
||||||
using YABA.Service.Interfaces;
|
using YABA.Service.Interfaces;
|
||||||
|
|
||||||
namespace YABA.API.Controllers
|
namespace YABA.API.Controllers
|
||||||
@ -24,10 +25,86 @@ namespace YABA.API.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
[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));
|
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()
|
public AutoMapperProfile()
|
||||||
{
|
{
|
||||||
CreateMap<UserDTO, UserResponse>();
|
CreateMap<UserDTO, UserResponse>();
|
||||||
CreateMap<TagSummaryDTO, TagResponse>();
|
|
||||||
CreateMap<BookmarkDTO, BookmarkResponse>();
|
CreateMap<BookmarkDTO, BookmarkResponse>();
|
||||||
CreateMap<WebsiteMetaDataDTO, GetWebsiteMetaDataResponse>();
|
CreateMap<WebsiteMetaDataDTO, GetWebsiteMetaDataResponse>();
|
||||||
CreateMap<BookmarkDTO, PatchBookmarkRequest>();
|
CreateMap<BookmarkDTO, PatchBookmarkRequest>();
|
||||||
CreateMap<PatchBookmarkRequest, UpdateBookmarkRequestDTO>();
|
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 int Id { get; set; }
|
||||||
public string Name { 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 string? Note { get; set; }
|
||||||
public bool IsHidden { get; set; }
|
public bool IsHidden { get; set; }
|
||||||
public string Url { 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 class TagDTO
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public bool IsDeleted { get; set; }
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool IsHidden { 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)
|
foreach (var bookmarkTag in bookmarkTags)
|
||||||
{
|
{
|
||||||
var tagDTO = _mapper.Map<TagSummaryDTO>(bookmarkTag.Tag);
|
var tagDTO = _mapper.Map<TagDTO>(bookmarkTag.Tag);
|
||||||
bookmarkDTO.Tags.Add(tagDTO);
|
bookmarkDTO.Tags.Add(tagDTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ namespace YABA.Service
|
|||||||
var currentUserId = GetCurrentUserId();
|
var currentUserId = GetCurrentUserId();
|
||||||
|
|
||||||
var bookmark = _context.Bookmarks.FirstOrDefault(x => x.UserId == currentUserId && x.Id == id);
|
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())
|
if (request.Tags != null && request.Tags.Any())
|
||||||
tags = (await UpdateBookmarkTags(id, request.Tags)).ToList();
|
tags = (await UpdateBookmarkTags(id, request.Tags)).ToList();
|
||||||
@ -126,7 +126,7 @@ namespace YABA.Service
|
|||||||
return null;
|
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();
|
var currentUserId = GetCurrentUserId();
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ namespace YABA.Service
|
|||||||
.Where(x => x.BookmarkId == id)
|
.Where(x => x.BookmarkId == id)
|
||||||
.Select(x => x.Tag);
|
.Select(x => x.Tag);
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<TagSummaryDTO>>(updatedBookmarkTags);
|
return _mapper.Map<IEnumerable<TagDTO>>(updatedBookmarkTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -183,12 +183,12 @@ namespace YABA.Service
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
||||||
bookmarkDTO.Tags = _mapper.Map<List<TagSummaryDTO>>(bookmarkTags);
|
bookmarkDTO.Tags = _mapper.Map<List<TagDTO>>(bookmarkTags);
|
||||||
|
|
||||||
return bookmarkDTO;
|
return bookmarkDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<TagSummaryDTO>? GetBookmarkTags(int id)
|
public IEnumerable<TagDTO>? GetBookmarkTags(int id)
|
||||||
{
|
{
|
||||||
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
||||||
if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == userId)) return null;
|
if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == userId)) return null;
|
||||||
@ -199,7 +199,7 @@ namespace YABA.Service
|
|||||||
.Select(x => x.Tag)
|
.Select(x => x.Tag)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<TagSummaryDTO>>(bookmarkTags);
|
return _mapper.Map<IEnumerable<TagDTO>>(bookmarkTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int?> DeleteBookmark(int id)
|
public async Task<int?> DeleteBookmark(int id)
|
||||||
@ -236,6 +236,20 @@ namespace YABA.Service
|
|||||||
return entriesToHide.Select(x => x.Id);
|
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()
|
private int GetCurrentUserId()
|
||||||
{
|
{
|
||||||
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
||||||
|
|||||||
@ -15,7 +15,8 @@ namespace YABA.Service.Configuration
|
|||||||
CreateMap<Bookmark, BookmarkDTO>();
|
CreateMap<Bookmark, BookmarkDTO>();
|
||||||
CreateMap<CreateBookmarkRequestDTO, Bookmark>();
|
CreateMap<CreateBookmarkRequestDTO, Bookmark>();
|
||||||
CreateMap<Tag, TagDTO>();
|
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.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using YABA.Common.DTOs;
|
|
||||||
using YABA.Common.DTOs.Bookmarks;
|
using YABA.Common.DTOs.Bookmarks;
|
||||||
using YABA.Common.DTOs.Tags;
|
using YABA.Common.DTOs.Tags;
|
||||||
|
|
||||||
@ -10,13 +9,14 @@ namespace YABA.Service.Interfaces
|
|||||||
{
|
{
|
||||||
Task<BookmarkDTO?> CreateBookmark(CreateBookmarkRequestDTO request);
|
Task<BookmarkDTO?> CreateBookmark(CreateBookmarkRequestDTO request);
|
||||||
Task<BookmarkDTO?> UpdateBookmark(int id, UpdateBookmarkRequestDTO 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);
|
IEnumerable<BookmarkDTO> GetAll(bool isHidden = false);
|
||||||
Task<BookmarkDTO?> Get(int id);
|
Task<BookmarkDTO?> Get(int id);
|
||||||
IEnumerable<TagSummaryDTO>? GetBookmarkTags(int id);
|
IEnumerable<TagDTO>? GetBookmarkTags(int id);
|
||||||
Task<int?> DeleteBookmark(int id);
|
Task<int?> DeleteBookmark(int id);
|
||||||
Task<IEnumerable<int>?> DeleteBookmarks(IEnumerable<int> ids);
|
Task<IEnumerable<int>?> DeleteBookmarks(IEnumerable<int> ids);
|
||||||
Task<IEnumerable<int>?> HideBookmarks(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.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using YABA.Common.DTOs.Tags;
|
using YABA.Common.DTOs.Tags;
|
||||||
|
|
||||||
namespace YABA.Service.Interfaces
|
namespace YABA.Service.Interfaces
|
||||||
{
|
{
|
||||||
public interface ITagsService
|
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 AutoMapper;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using YABA.Common.DTOs.Tags;
|
using YABA.Common.DTOs.Tags;
|
||||||
using YABA.Common.Extensions;
|
using YABA.Common.Extensions;
|
||||||
using YABA.Data.Context;
|
using YABA.Data.Context;
|
||||||
|
using YABA.Data.Extensions;
|
||||||
|
using YABA.Models;
|
||||||
using YABA.Service.Interfaces;
|
using YABA.Service.Interfaces;
|
||||||
|
|
||||||
namespace YABA.Service
|
namespace YABA.Service
|
||||||
@ -13,29 +17,111 @@ namespace YABA.Service
|
|||||||
public class TagsService : ITagsService
|
public class TagsService : ITagsService
|
||||||
{
|
{
|
||||||
private readonly YABAReadOnlyContext _roContext;
|
private readonly YABAReadOnlyContext _roContext;
|
||||||
|
private readonly YABAReadWriteContext _rwContext;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
public TagsService(YABAReadOnlyContext roContext, IHttpContextAccessor httpContextAccessor, IMapper mapper)
|
public TagsService(
|
||||||
|
YABAReadOnlyContext roContext,
|
||||||
|
YABAReadWriteContext rwContext,
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
IMapper mapper)
|
||||||
{
|
{
|
||||||
_roContext = roContext;
|
_roContext = roContext;
|
||||||
|
_rwContext = rwContext;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_mapper = mapper;
|
_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 currentUserId = GetCurrentUserId();
|
||||||
|
|
||||||
var activeUserTags = _roContext.BookmarkTags
|
if(!(await _roContext.Users.UserExistsAsync(currentUserId))
|
||||||
.Include(x => x.Tag)
|
|| await _roContext.Tags.AnyAsync(x => x.UserId == currentUserId && x.Name.ToLower() == request.Name.ToLower()))
|
||||||
.Where(x => x.Tag.UserId == currentUserId)
|
return null;
|
||||||
.ToList()
|
|
||||||
.GroupBy(x => x.Tag.Id)
|
|
||||||
.Select(g => g.First()?.Tag);
|
|
||||||
|
|
||||||
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()
|
private int GetCurrentUserId()
|
||||||
|
|||||||
@ -1,22 +1,23 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import { Footer, Header, ProtectedRoute } from './components';
|
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";
|
import { isDev } from "./utils";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App" style={{minHeight: '100vh', display: 'flex', flexDirection: 'column'}}>
|
<div className="App" style={{minHeight: '100vh', display: 'flex', flexDirection: 'column'}}>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={<BaseLayout header={ <Header />} content={ <HomeView /> } footer={ <Footer />}/>}
|
element={<BaseLayout header={ <Header />} content={ <HomeView /> } footer={ <Footer />}/>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/redirect"
|
path="/redirect"
|
||||||
element={<BaseLayout header={ <Header />} content={ <RedirectView />} footer={ <Footer />}/> }
|
element={<BaseLayout header={ <Header />} content={ <RedirectView />} footer={ <Footer />}/> }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/bookmarks"
|
path="/bookmarks"
|
||||||
element={
|
element={
|
||||||
@ -29,6 +30,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/bookmarks/hidden"
|
path="/bookmarks/hidden"
|
||||||
element={
|
element={
|
||||||
@ -41,6 +43,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/bookmarks/new"
|
path="/bookmarks/new"
|
||||||
element={
|
element={
|
||||||
@ -52,6 +55,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/bookmarks/:id"
|
path="/bookmarks/:id"
|
||||||
element={
|
element={
|
||||||
@ -63,16 +67,30 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/404"
|
path="/404"
|
||||||
element={<BaseLayout content={<NotFoundView />} />}
|
element={<BaseLayout content={<NotFoundView />} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ isDev() && (
|
{ isDev() && (
|
||||||
<Route
|
<Route
|
||||||
path="/test"
|
path="/test"
|
||||||
element={<BaseLayout header={ <Header />} content={ <TestView /> } footer={ <Footer />}/>}
|
element={<BaseLayout header={ <Header />} content={ <TestView /> } footer={ <Footer />}/>}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/tags"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute
|
||||||
|
layout={BaseLayout}
|
||||||
|
header={Header}
|
||||||
|
footer={Footer}
|
||||||
|
view={TagsView}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { callExternalApi } from "./apiHelper";
|
import { callExternalApi } from "./apiHelper";
|
||||||
import { getAllBookmarks, getBookmark, createNewBookmark, updateBookmark, deleteBookmark, deleteBookmarks, hideBookmarks } from "./v1/bookmarks";
|
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";
|
import { getWebsiteMetaData } from "./v1/misc";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -10,8 +10,12 @@ export {
|
|||||||
createNewBookmark,
|
createNewBookmark,
|
||||||
updateBookmark,
|
updateBookmark,
|
||||||
getWebsiteMetaData,
|
getWebsiteMetaData,
|
||||||
getAllTags,
|
|
||||||
deleteBookmark,
|
deleteBookmark,
|
||||||
deleteBookmarks,
|
deleteBookmarks,
|
||||||
hideBookmarks,
|
hideBookmarks,
|
||||||
|
getAllTags,
|
||||||
|
createNewTag,
|
||||||
|
updateTag,
|
||||||
|
deleteTags,
|
||||||
|
hideTags,
|
||||||
};
|
};
|
||||||
@ -13,4 +13,64 @@ export const getAllTags = async(accessToken) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return await callExternalApi({config});
|
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 Form from 'react-bootstrap/Form';
|
||||||
import Button from "react-bootstrap/Button";
|
import Button from "react-bootstrap/Button";
|
||||||
import Modal from 'react-bootstrap/Modal'
|
import Modal from 'react-bootstrap/Modal'
|
||||||
|
import { Table } from "react-bootstrap";
|
||||||
|
import Dropdown from 'react-bootstrap/Dropdown';
|
||||||
|
import DropdownButton from 'react-bootstrap/DropdownButton';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Alert,
|
Alert,
|
||||||
@ -14,4 +17,7 @@ export {
|
|||||||
Form,
|
Form,
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
|
Table,
|
||||||
|
Dropdown,
|
||||||
|
DropdownButton,
|
||||||
};
|
};
|
||||||
@ -57,6 +57,7 @@ export function Header(props) {
|
|||||||
<NavDropdown.Divider />
|
<NavDropdown.Divider />
|
||||||
<NavDropdown.Item href="/bookmarks/new">New</NavDropdown.Item>
|
<NavDropdown.Item href="/bookmarks/new">New</NavDropdown.Item>
|
||||||
</NavDropdown>
|
</NavDropdown>
|
||||||
|
<Nav.Link href="/tags">Tags</Nav.Link>
|
||||||
<Nav.Link onClick={handleLogout}>Logout</Nav.Link>
|
<Nav.Link onClick={handleLogout}>Logout</Nav.Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { Auth0ProviderWithNavigate } from "./auth0ProviderWithNavigate";
|
|||||||
import { ProtectedRoute } from "./protectedRoute";
|
import { ProtectedRoute } from "./protectedRoute";
|
||||||
import { SplashScreen } from "./shared/splashScreen";
|
import { SplashScreen } from "./shared/splashScreen";
|
||||||
import { SearchForm } from "./shared/searchForm";
|
import { SearchForm } from "./shared/searchForm";
|
||||||
|
import { UpsertTagModal } from "./tags/upsertTagModal";
|
||||||
|
import { InterceptModal } from "./shared/interceptModal";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Footer,
|
Footer,
|
||||||
@ -13,5 +15,7 @@ export {
|
|||||||
Auth0ProviderWithNavigate,
|
Auth0ProviderWithNavigate,
|
||||||
ProtectedRoute,
|
ProtectedRoute,
|
||||||
SplashScreen,
|
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 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 { Bookmark } from "../components";
|
||||||
import { useAuth0 } from "@auth0/auth0-react";
|
import { useAuth0 } from "@auth0/auth0-react";
|
||||||
import { deleteBookmarks, getAllBookmarks, getAllTags, hideBookmarks } from "../api";
|
import { deleteBookmarks, getAllBookmarks, getAllTags, hideBookmarks } from "../api";
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { BookmarksListView } from "./bookmarksListView";
|
|||||||
import { BookmarkDetailView } from "./bookmarkDetailView";
|
import { BookmarkDetailView } from "./bookmarkDetailView";
|
||||||
import { TestView } from "./testView";
|
import { TestView } from "./testView";
|
||||||
import { NotFoundView } from "./notFoundView";
|
import { NotFoundView } from "./notFoundView";
|
||||||
|
import { TagsView } from "./tagsView";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BaseLayout,
|
BaseLayout,
|
||||||
@ -13,5 +14,6 @@ export {
|
|||||||
BookmarksListView,
|
BookmarksListView,
|
||||||
BookmarkDetailView,
|
BookmarkDetailView,
|
||||||
TestView,
|
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