Modified endpoint to properly filter Bookmarks based on Hidden flag on both the Bookmark entry itself and its associated tags

This commit is contained in:
2023-02-20 22:48:52 -06:00
parent c55b018c0d
commit 83a4fba905
7 changed files with 57 additions and 84 deletions

View File

@ -74,9 +74,9 @@ namespace YABA.API.Controllers
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(IEnumerable<BookmarkResponse>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<BookmarkResponse>), (int)HttpStatusCode.OK)]
public IActionResult GetAll(bool isHidden = false) public IActionResult GetAll(bool showHidden = false)
{ {
var result = _bookmarkService.GetAll(isHidden); var result = _bookmarkService.GetAll(showHidden);
return Ok(_mapper.Map<IEnumerable<BookmarkResponse>>(result)); return Ok(_mapper.Map<IEnumerable<BookmarkResponse>>(result));
} }

View File

@ -1,13 +0,0 @@
namespace YABA.API
{
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}

View File

@ -37,35 +37,29 @@ namespace YABA.Service
_mapper = mapper; _mapper = mapper;
} }
public IEnumerable<BookmarkDTO> GetAll(bool isHidden = false) public IEnumerable<BookmarkDTO> GetAll(bool showHidden = false)
{ {
var currentUserId = GetCurrentUserId(); var currentUserId = GetCurrentUserId();
var userBookmarks = _roContext.Bookmarks.Where(x => x.UserId == currentUserId && x.IsHidden == isHidden).ToDictionary(k => k.Id, v => v);
var bookmarkTagsLookup = _roContext.BookmarkTags var bookmarkTagsLookup = _roContext.BookmarkTags
.Include(x => x.Tag) .Include(x => x.Bookmark)
.Where(x => userBookmarks.Keys.Contains(x.BookmarkId)) .Include(x => x.Tag)
.ToList() .Where(x => x.Bookmark.UserId == currentUserId && x.Tag.UserId == currentUserId)
.GroupBy(x => x.BookmarkId) .ToList()
.ToDictionary(k => k.Key, v => v.ToList()); .GroupBy(x => x.BookmarkId)
.ToDictionary(k => k.Key, v => new KeyValuePair<Bookmark, List<Tag>>(v.FirstOrDefault()?.Bookmark, v.Select(x => x.Tag).ToList()));
var userBookmarkDTOs = new List<BookmarkDTO>(); var userBookmarkDTOs = new List<BookmarkDTO>();
foreach(var bookmark in userBookmarks) foreach (var bookmarkTag in bookmarkTagsLookup)
{ {
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark.Value); if ((showHidden && (bookmarkTag.Value.Key.IsHidden || bookmarkTag.Value.Value.Any(x => x.IsHidden))) // If showHidden = true, only add Bookmarks that are marked Hidden OR when any of its Tags are marked Hidden
bookmarkTagsLookup.TryGetValue(bookmark.Key, out var bookmarkTags); || (!showHidden && !bookmarkTag.Value.Key.IsHidden && bookmarkTag.Value.Value.All(x => !x.IsHidden))) // If showHidden = false, only add when Bookmark is not marked Hidden AND when any of its Tags are not marked as Hidden
if(bookmarkTags != null)
{ {
foreach (var bookmarkTag in bookmarkTags) var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmarkTag.Value.Key);
{ var bookmarkTagDTOs = _mapper.Map<IEnumerable<TagDTO>>(bookmarkTag.Value.Value);
var tagDTO = _mapper.Map<TagDTO>(bookmarkTag.Tag); bookmarkDTO.Tags.AddRange(bookmarkTagDTOs);
bookmarkDTO.Tags.Add(tagDTO); userBookmarkDTOs.Add(bookmarkDTO);
}
} }
userBookmarkDTOs.Add(bookmarkDTO);
} }
return userBookmarkDTOs; return userBookmarkDTOs;

View File

@ -2,9 +2,9 @@ import { callExternalApi } from "../apiHelper";
const apiServerUrl = `${process.env.REACT_APP_API_BASE_URL}/v1/Bookmarks`; const apiServerUrl = `${process.env.REACT_APP_API_BASE_URL}/v1/Bookmarks`;
export const getAllBookmarks = async(accessToken, isHidden = false) => { export const getAllBookmarks = async(accessToken, showHidden = false) => {
const config = { const config = {
url: `${apiServerUrl}?isHidden=${isHidden}`, url: `${apiServerUrl}?showHidden=${showHidden}`,
method: "GET", method: "GET",
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",

View File

@ -1,5 +1,5 @@
import { isDev } from "./isDevHelper"; import { isDev } from "./isDevHelper";
import { getTagGroups } from "./tagsHelper"; import { getTagGroups, flattenTagArrays } from "./tagsHelper";
import { isSubset } from "./arrayHelper"; import { isSubset } from "./arrayHelper";
import { containsSubstring } from "./bookmarkHelper"; import { containsSubstring } from "./bookmarkHelper";
@ -7,5 +7,6 @@ export {
isDev, isDev,
getTagGroups, getTagGroups,
isSubset, isSubset,
containsSubstring containsSubstring,
flattenTagArrays
} }

View File

@ -14,4 +14,12 @@ export const getTagGroups = (allBookmarkTags) => {
return accumulator return accumulator
}, []).sort((a, b) => (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0); }, []).sort((a, b) => (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0);
} };
export const flattenTagArrays = (allBookmarkTags) => allBookmarkTags.flat().reduce((accumulator, current) => {
if(!accumulator.find((item) => item.id === current.id)) {
accumulator.push(current);
}
return accumulator
}, []);

View File

@ -2,9 +2,9 @@ import React, { useEffect, useReducer, useState } from "react";
import { Alert, Col, Container, Row, 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, hideBookmarks } from "../api";
import { SplashScreen, SearchForm } from "../components"; import { SplashScreen, SearchForm } from "../components";
import { getTagGroups, isSubset, containsSubstring } from "../utils"; import { getTagGroups, isSubset, containsSubstring, flattenTagArrays } from "../utils";
export function BookmarksListView(props) { export function BookmarksListView(props) {
const { getAccessTokenSilently } = useAuth0(); const { getAccessTokenSilently } = useAuth0();
@ -65,34 +65,9 @@ export function BookmarksListView(props) {
}); });
} }
}; };
const [alertMessageState, dispatchAlertMessageState] = useReducer(alertReducer, alertMessageInitialState); const [alertMessageState, dispatchAlertMessageState] = useReducer(alertReducer, alertMessageInitialState);
const tagsInitialState = []; const bookmarksReducer = (state = [], action) => {
const tagsReducer = (state = tagsInitialState, action) => {
switch(action.type) {
case "SET":
return action.payload.tags.map(x => ({...x, isSelected: false }));
case "ADD_SELECTED":
case "REMOVE_SELECTED":
const modifiedTags = [...state];
const selectedTagIndex = modifiedTags.findIndex((x) => x.id === action.payload.selectedTagId);
modifiedTags[selectedTagIndex].isSelected = action.type === "ADD_SELECTED";
return modifiedTags;
default:
return state;
}
}
const [tagsState, dispatchTagsState] = useReducer(tagsReducer, tagsInitialState);
const onTagSelected = (isTagSelected, tag) => dispatchTagsState({type: isTagSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedTagId: tag.id}})
const getSelectedTags = () => tagsState.filter((x) => x.isSelected);
const getNotSelectedTags = () => tagsState.filter((x) => !x.isSelected);
const bookmarksInitialState = [];
const bookmarksReducer = (state = bookmarksInitialState, action) => {
switch(action.type) { switch(action.type) {
case "SET": case "SET":
return action.payload.bookmarks.map(x => ({...x, isSelected: false, isDisplayed: true})); return action.payload.bookmarks.map(x => ({...x, isSelected: false, isDisplayed: true}));
@ -122,7 +97,7 @@ export function BookmarksListView(props) {
} }
}; };
const [bookmarksState, dispatchBookmarksState] = useReducer(bookmarksReducer, bookmarksInitialState); const [bookmarksState, dispatchBookmarksState] = useReducer(bookmarksReducer, []);
const onBookmarkSelected = (isBookmarkSelected, bookmark) => dispatchBookmarksState({type: isBookmarkSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedBookmarkId: bookmark.id}}); const onBookmarkSelected = (isBookmarkSelected, bookmark) => dispatchBookmarksState({type: isBookmarkSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedBookmarkId: bookmark.id}});
const getSelectedBookmarksCount = () => bookmarksState.filter((x) => x.isSelected).length; const getSelectedBookmarksCount = () => bookmarksState.filter((x) => x.isSelected).length;
const getSelectedBookmarks = () => bookmarksState.filter((x) => x.isSelected); const getSelectedBookmarks = () => bookmarksState.filter((x) => x.isSelected);
@ -205,17 +180,6 @@ export function BookmarksListView(props) {
}; };
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}});
}
};
const fetchBookmarks = async() => { const fetchBookmarks = async() => {
const accessToken = await getAccessTokenSilently(); const accessToken = await getAccessTokenSilently();
const { data, error } = await getAllBookmarks(accessToken, props.showHidden); const { data, error } = await getAllBookmarks(accessToken, props.showHidden);
@ -224,13 +188,32 @@ export function BookmarksListView(props) {
dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "danger", "message": `Error fetching bookmarks: ${error.message}`}}); dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "danger", "message": `Error fetching bookmarks: ${error.message}`}});
} else { } else {
dispatchBookmarksState({type: "SET", payload: {bookmarks: data}}); dispatchBookmarksState({type: "SET", payload: {bookmarks: data}});
dispatchTagsState({type: "SET"});
} }
} }
useEffect(() => {
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Retrieving Tags..."}});
fetchTags();
const tagsReducer = (state = [], action) => {
switch(action.type) {
case "SET":
return flattenTagArrays(bookmarksState.map(x => x.tags)).map(x => Object.assign({}, x, {isSelected : false}));
case "ADD_SELECTED":
case "REMOVE_SELECTED":
const modifiedTags = [...state];
const selectedTagIndex = modifiedTags.findIndex((x) => x.id === action.payload.selectedTagId);
modifiedTags[selectedTagIndex].isSelected = action.type === "ADD_SELECTED";
return modifiedTags;
default:
return state;
}
}
const [tagsState, dispatchTagsState] = useReducer(tagsReducer, []);
const onTagSelected = (isTagSelected, tag) => dispatchTagsState({type: isTagSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedTagId: tag.id}})
const getSelectedTags = () => tagsState.filter((x) => x.isSelected);
const getNotSelectedTags = () => tagsState.filter((x) => !x.isSelected);
useEffect(() => {
dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Retrieving Bookmarks..."}}); dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Retrieving Bookmarks..."}});
fetchBookmarks(); fetchBookmarks();
@ -354,7 +337,7 @@ export function BookmarksListView(props) {
<br /> <br />
{ {
group.tags.map((tag) => { group.tags.map((tag) => {
return <Button key={tag.id} variant="link" style={{textDecoration: "none"}} className="ms-0 me-2 p-0" onClick={() => onTagSelected(true, tag)}> return <Button key={tag.id} variant="link" style={{textDecoration: "none"}} className={`ms-0 me-2 p-0 ${tag.isHidden ? "text-danger" : null}`} onClick={() => onTagSelected(true, tag)}>
#{tag.name} #{tag.name}
</Button> </Button>
}) })