From 83a4fba9059cf735c130cbc305cba6005d07534d Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Mon, 20 Feb 2023 22:48:52 -0600 Subject: [PATCH 1/8] Modified endpoint to properly filter Bookmarks based on Hidden flag on both the Bookmark entry itself and its associated tags --- YABA.API/Controllers/BookmarksController.cs | 4 +- YABA.API/WeatherForecast.cs | 13 ---- YABA.Service/BookmarkService.cs | 34 ++++------ yaba-web/src/api/v1/bookmarks.js | 4 +- yaba-web/src/utils/index.js | 5 +- yaba-web/src/utils/tagsHelper.js | 10 ++- yaba-web/src/views/bookmarksListView.jsx | 71 ++++++++------------- 7 files changed, 57 insertions(+), 84 deletions(-) delete mode 100644 YABA.API/WeatherForecast.cs diff --git a/YABA.API/Controllers/BookmarksController.cs b/YABA.API/Controllers/BookmarksController.cs index 703f016..8792c4e 100644 --- a/YABA.API/Controllers/BookmarksController.cs +++ b/YABA.API/Controllers/BookmarksController.cs @@ -74,9 +74,9 @@ namespace YABA.API.Controllers [HttpGet] [ProducesResponseType(typeof(IEnumerable), (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>(result)); } diff --git a/YABA.API/WeatherForecast.cs b/YABA.API/WeatherForecast.cs deleted file mode 100644 index 59b87e7..0000000 --- a/YABA.API/WeatherForecast.cs +++ /dev/null @@ -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; } - } -} \ No newline at end of file diff --git a/YABA.Service/BookmarkService.cs b/YABA.Service/BookmarkService.cs index 7c4d098..8fc333a 100644 --- a/YABA.Service/BookmarkService.cs +++ b/YABA.Service/BookmarkService.cs @@ -37,35 +37,29 @@ namespace YABA.Service _mapper = mapper; } - public IEnumerable GetAll(bool isHidden = false) + public IEnumerable GetAll(bool showHidden = false) { 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 - .Include(x => x.Tag) - .Where(x => userBookmarks.Keys.Contains(x.BookmarkId)) - .ToList() - .GroupBy(x => x.BookmarkId) - .ToDictionary(k => k.Key, v => v.ToList()); - + .Include(x => x.Bookmark) + .Include(x => x.Tag) + .Where(x => x.Bookmark.UserId == currentUserId && x.Tag.UserId == currentUserId) + .ToList() + .GroupBy(x => x.BookmarkId) + .ToDictionary(k => k.Key, v => new KeyValuePair>(v.FirstOrDefault()?.Bookmark, v.Select(x => x.Tag).ToList())); var userBookmarkDTOs = new List(); - foreach(var bookmark in userBookmarks) + foreach (var bookmarkTag in bookmarkTagsLookup) { - var bookmarkDTO = _mapper.Map(bookmark.Value); - bookmarkTagsLookup.TryGetValue(bookmark.Key, out var bookmarkTags); - - if(bookmarkTags != null) + 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 + || (!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 { - foreach (var bookmarkTag in bookmarkTags) - { - var tagDTO = _mapper.Map(bookmarkTag.Tag); - bookmarkDTO.Tags.Add(tagDTO); - } + var bookmarkDTO = _mapper.Map(bookmarkTag.Value.Key); + var bookmarkTagDTOs = _mapper.Map>(bookmarkTag.Value.Value); + bookmarkDTO.Tags.AddRange(bookmarkTagDTOs); + userBookmarkDTOs.Add(bookmarkDTO); } - - userBookmarkDTOs.Add(bookmarkDTO); } return userBookmarkDTOs; diff --git a/yaba-web/src/api/v1/bookmarks.js b/yaba-web/src/api/v1/bookmarks.js index dce5b21..928c367 100644 --- a/yaba-web/src/api/v1/bookmarks.js +++ b/yaba-web/src/api/v1/bookmarks.js @@ -2,9 +2,9 @@ import { callExternalApi } from "../apiHelper"; 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 = { - url: `${apiServerUrl}?isHidden=${isHidden}`, + url: `${apiServerUrl}?showHidden=${showHidden}`, method: "GET", headers: { "content-type": "application/json", diff --git a/yaba-web/src/utils/index.js b/yaba-web/src/utils/index.js index ca6cfa9..523bb57 100644 --- a/yaba-web/src/utils/index.js +++ b/yaba-web/src/utils/index.js @@ -1,5 +1,5 @@ import { isDev } from "./isDevHelper"; -import { getTagGroups } from "./tagsHelper"; +import { getTagGroups, flattenTagArrays } from "./tagsHelper"; import { isSubset } from "./arrayHelper"; import { containsSubstring } from "./bookmarkHelper"; @@ -7,5 +7,6 @@ export { isDev, getTagGroups, isSubset, - containsSubstring + containsSubstring, + flattenTagArrays } \ No newline at end of file diff --git a/yaba-web/src/utils/tagsHelper.js b/yaba-web/src/utils/tagsHelper.js index b0da23a..dd50ed0 100644 --- a/yaba-web/src/utils/tagsHelper.js +++ b/yaba-web/src/utils/tagsHelper.js @@ -14,4 +14,12 @@ export const getTagGroups = (allBookmarkTags) => { return accumulator }, []).sort((a, b) => (a.name < b.name) ? -1 : (a.name > b.name) ? 1 : 0); -} \ No newline at end of file +}; + +export const flattenTagArrays = (allBookmarkTags) => allBookmarkTags.flat().reduce((accumulator, current) => { + if(!accumulator.find((item) => item.id === current.id)) { + accumulator.push(current); + } + + return accumulator +}, []); \ No newline at end of file diff --git a/yaba-web/src/views/bookmarksListView.jsx b/yaba-web/src/views/bookmarksListView.jsx index b87367c..acfa1ce 100644 --- a/yaba-web/src/views/bookmarksListView.jsx +++ b/yaba-web/src/views/bookmarksListView.jsx @@ -2,9 +2,9 @@ import React, { useEffect, useReducer, useState } from "react"; 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"; +import { deleteBookmarks, getAllBookmarks, hideBookmarks } from "../api"; import { SplashScreen, SearchForm } from "../components"; -import { getTagGroups, isSubset, containsSubstring } from "../utils"; +import { getTagGroups, isSubset, containsSubstring, flattenTagArrays } from "../utils"; export function BookmarksListView(props) { const { getAccessTokenSilently } = useAuth0(); @@ -65,34 +65,9 @@ export function BookmarksListView(props) { }); } }; - const [alertMessageState, dispatchAlertMessageState] = useReducer(alertReducer, alertMessageInitialState); - const tagsInitialState = []; - - 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) => { + const bookmarksReducer = (state = [], action) => { switch(action.type) { case "SET": 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 getSelectedBookmarksCount = () => bookmarksState.filter((x) => x.isSelected).length; 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 accessToken = await getAccessTokenSilently(); 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}`}}); } else { dispatchBookmarksState({type: "SET", payload: {bookmarks: data}}); + dispatchTagsState({type: "SET"}); + } + } + + + 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; } } - useEffect(() => { - dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Retrieving Tags..."}}); - fetchTags(); + 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..."}}); fetchBookmarks(); @@ -354,7 +337,7 @@ export function BookmarksListView(props) {
{ group.tags.map((tag) => { - return }) -- 2.49.0 From 0065613cdd64f622a590c21fb402285c9c5a69c5 Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Tue, 21 Feb 2023 20:32:28 -0600 Subject: [PATCH 2/8] Modified to fix search and tag filtering not working together, fixed proper count for selected items to not include not displayed items --- yaba-web/src/views/bookmarksListView.jsx | 174 +++++++++++------------ 1 file changed, 86 insertions(+), 88 deletions(-) diff --git a/yaba-web/src/views/bookmarksListView.jsx b/yaba-web/src/views/bookmarksListView.jsx index acfa1ce..c2667bc 100644 --- a/yaba-web/src/views/bookmarksListView.jsx +++ b/yaba-web/src/views/bookmarksListView.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useReducer, useState } from "react"; -import { Alert, Col, Container, Row, Button, Modal } from "../components/external"; +import { Alert, Col, Container, Row, Button, Modal, Dropdown, DropdownButton } from "../components/external"; import { Bookmark } from "../components"; import { useAuth0 } from "@auth0/auth0-react"; import { deleteBookmarks, getAllBookmarks, hideBookmarks } from "../api"; @@ -9,24 +9,6 @@ import { getTagGroups, isSubset, containsSubstring, flattenTagArrays } from "../ export function BookmarksListView(props) { const { getAccessTokenSilently } = useAuth0(); const [searchString, setSearchString] = useState(""); - - const handleSearch = (e) => { - e.preventDefault(); - - if(!searchString) { - dispatchBookmarksState({type: "DISPLAY_ALL"}); - } else { - dispatchBookmarksState({type: "SEARCH"}); - } - }; - - const handleSearchStringChange = (e) => { - if(!e.target.value) { - dispatchBookmarksState({type: "DISPLAY_ALL"}); - } else { - setSearchString(e.target.value); - } - }; const initialSplashScreenState = { show: false, message: null }; @@ -67,7 +49,30 @@ export function BookmarksListView(props) { }; const [alertMessageState, dispatchAlertMessageState] = useReducer(alertReducer, alertMessageInitialState); - const bookmarksReducer = (state = [], action) => { + const tagsInitialState = []; + + const tagsReducer = (state = tagsInitialState, action) => { + switch(action.type) { + case "SET": + return action.payload.tags.map(x => ({...x, isSelected: false, isDisplayed: true })); + 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 getSelectedTags = () => tagsState.filter((x) => x.isSelected); + const getNotSelectedTags = () => tagsState.filter((x) => !x.isSelected); + + const bookmarksInitialState = []; + + const bookmarksReducer = (state = bookmarksInitialState, action) => { switch(action.type) { case "SET": return action.payload.bookmarks.map(x => ({...x, isSelected: false, isDisplayed: true})); @@ -81,17 +86,14 @@ export function BookmarksListView(props) { case "HIDE_SELECTED": return state.filter((x) => !action.payload.selectedBookmarkIds.includes(x.id)); case "SELECT_ALL": - return state.map(x => ({...x, isSelected: true})); + return state.map(x => ({...x, isSelected: x.isDisplayed})); case "UNSELECT_ALL": return state.map(x => ({...x, isSelected: false})); case "DISPLAY_ALL": return state.map(x => ({...x, isDisplayed: true})); case "SEARCH": - if(!searchString) { - dispatchBookmarksState({type: "DISPLAY_ALL"}); - } - - return state.map(x => ({...x, isDisplayed: containsSubstring(x, searchString)})); + return state.map(x => ({...x, isDisplayed: (getSelectedTags().length <= 0 || isSubset(x.tags.map(x => x.id), getSelectedTags().map(x => x.id))) + && (!searchString || containsSubstring(x, searchString))})); default: return state; } @@ -99,16 +101,11 @@ export function BookmarksListView(props) { const [bookmarksState, dispatchBookmarksState] = useReducer(bookmarksReducer, []); const onBookmarkSelected = (isBookmarkSelected, bookmark) => dispatchBookmarksState({type: isBookmarkSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedBookmarkId: bookmark.id}}); - const getSelectedBookmarksCount = () => bookmarksState.filter((x) => x.isSelected).length; - const getSelectedBookmarks = () => bookmarksState.filter((x) => x.isSelected); - const getAreAllBookmarksSelected = () => bookmarksState.every(x => x.isSelected); - const getFilteredBookmarks = () => { - if (getSelectedTags().length <= 0) { - return bookmarksState.filter(x => x.isDisplayed); - } else { - return bookmarksState.filter(x => isSubset(x.tags.map(x => x.id), getSelectedTags().map(x => x.id))); - } - } + const getDisplayedBookmarksCount = () => bookmarksState.filter(x => x.isDisplayed).length; + const getSelectedBookmarksCount = () => bookmarksState.filter(x => x.isSelected && x.isDisplayed).length; + const getSelectedBookmarks = () => bookmarksState.filter((x) => x.isSelected && x.isDisplayed); + const getAreAllBookmarksSelected = () => bookmarksState.filter(x => x.isDisplayed).every(x => x.isSelected); + const getFilteredBookmarks = () => bookmarksState.filter(x => x.isDisplayed); const onDeleteSelectedBookmarks = async (ids) => { if(ids.length <= 0) { @@ -180,44 +177,40 @@ export function BookmarksListView(props) { }; - const fetchBookmarks = async() => { - const accessToken = await getAccessTokenSilently(); - const { data, error } = await getAllBookmarks(accessToken, props.showHidden); + const onTagSelected = (isTagSelected, tag) => { + dispatchTagsState({type: isTagSelected ? "ADD_SELECTED" : "REMOVE_SELECTED", payload: {selectedTagId: tag.id}}); + dispatchBookmarksState({type: "SEARCH"}); + }; - if(error) { - dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "danger", "message": `Error fetching bookmarks: ${error.message}`}}); - } else { - dispatchBookmarksState({type: "SET", payload: {bookmarks: data}}); - dispatchTagsState({type: "SET"}); - } - } + const handleSearch = (e) => { + e.preventDefault(); + setSearchString(e.target[0].value); + dispatchBookmarksState({type: "SEARCH"}); + }; - - 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); + const handleSearchStringChange = (e) => { + setSearchString(e.target.value); + dispatchBookmarksState({type: "SEARCH"}); + }; useEffect(() => { dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Retrieving Bookmarks..."}}); - fetchBookmarks(); - dispatchSplashScreenState({type: "HIDE_SPLASH_SCREEN", payload: {message: null}}); + const fetchBookmarks = async() => { + const accessToken = await getAccessTokenSilently(); + const { data, error } = await getAllBookmarks(accessToken, props.showHidden); + + if(error) { + dispatchAlertMessageState({type: "SHOW_ALERT", payload: {show: true, alertType: "danger", "message": `Error fetching bookmarks: ${error.message}`}}); + } else { + dispatchBookmarksState({type: "SET", payload: {bookmarks: data}}); + dispatchTagsState({type: "SET", payload: {tags: flattenTagArrays(data.map(x => x.tags))}}); + } + + dispatchSplashScreenState({type: "HIDE_SPLASH_SCREEN", payload: {message: null}}); + } + + fetchBookmarks(); }, []); return( @@ -284,26 +277,31 @@ export function BookmarksListView(props) { - - { - bookmarksState.length > 0 && - - } - - - { - getSelectedBookmarksCount() > 0 && -
- {getSelectedBookmarksCount()} selected - - -
- } + +
+ { + getDisplayedBookmarksCount() <= 0 && + No bookmarks to display + } + + { + getDisplayedBookmarksCount() > 0 && + + } + + { + getSelectedBookmarksCount() > 0 && + + handleHideBookmarks(getSelectedBookmarks().map(x => x.id))}> + {props.showHidden ? "Unhide" : "Hide"} + + + Delete + + + } + +
{ -- 2.49.0 From 36058cb2833315e5c4db6335102da2e227707c50 Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Tue, 21 Feb 2023 21:40:21 -0600 Subject: [PATCH 3/8] Modified Auth0 integration to pull refresh token for auth and not hit Auth0 endpoints at every navigation page YABA-5 --- yaba-web/src/components/auth0ProviderWithNavigate.jsx | 2 ++ yaba-web/src/components/header.jsx | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/yaba-web/src/components/auth0ProviderWithNavigate.jsx b/yaba-web/src/components/auth0ProviderWithNavigate.jsx index 48e7c3e..fe625e3 100644 --- a/yaba-web/src/components/auth0ProviderWithNavigate.jsx +++ b/yaba-web/src/components/auth0ProviderWithNavigate.jsx @@ -27,6 +27,8 @@ export const Auth0ProviderWithNavigate = ({ children }) => { audience: audience }} onRedirectCallback={onRedirectCallback} + useRefreshTokens={true} + cacheLocation="localstorage" > {children} diff --git a/yaba-web/src/components/header.jsx b/yaba-web/src/components/header.jsx index b5ff55c..c93a198 100644 --- a/yaba-web/src/components/header.jsx +++ b/yaba-web/src/components/header.jsx @@ -11,7 +11,7 @@ export function Header(props) { const handleLogin = async () => { await loginWithRedirect({ appState: { - returnTo: "/bookmarks", + returnTo: "/", }, }); }; @@ -19,7 +19,7 @@ export function Header(props) { const handleSignUp = async () => { await loginWithRedirect({ appState: { - returnTo: "/bookmarks", + returnTo: "/", }, authorizationParams: { screen_hint: "signup", -- 2.49.0 From 868646845a6995a85187154aea912668d080d984 Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Tue, 21 Feb 2023 22:00:48 -0600 Subject: [PATCH 4/8] Replaced Button components representing Tags with reusable Tag component --- .../components/{bookmark => bookmarks}/bookmark.jsx | 7 ++++--- yaba-web/src/components/index.js | 4 +++- yaba-web/src/components/tags/tag.jsx | 13 +++++++++++++ yaba-web/src/views/bookmarksListView.jsx | 8 ++------ 4 files changed, 22 insertions(+), 10 deletions(-) rename yaba-web/src/components/{bookmark => bookmarks}/bookmark.jsx (87%) create mode 100644 yaba-web/src/components/tags/tag.jsx diff --git a/yaba-web/src/components/bookmark/bookmark.jsx b/yaba-web/src/components/bookmarks/bookmark.jsx similarity index 87% rename from yaba-web/src/components/bookmark/bookmark.jsx rename to yaba-web/src/components/bookmarks/bookmark.jsx index 80d8417..09ef9a0 100644 --- a/yaba-web/src/components/bookmark/bookmark.jsx +++ b/yaba-web/src/components/bookmarks/bookmark.jsx @@ -3,6 +3,7 @@ import { Row, Col, Form, Button } from "../external"; import DateTimeHelper from "../../utils/dateTimeHelper"; import "../../styles/component/bookmark.css"; import { useNavigate } from "react-router-dom"; +import { Tag } from "../../components"; export function Bookmark(props) { const navigate = useNavigate(); @@ -16,9 +17,9 @@ export function Bookmark(props) { {props.bookmark.title}
{props.bookmark.description.substring(0, 100)}
- {props.bookmark.tags.map((tag) => { - return - })} + { + props.bookmark.tags.map((tag) => ) + }
{DateTimeHelper.getFriendlyDate(props.bookmark.createdOn)} | diff --git a/yaba-web/src/components/index.js b/yaba-web/src/components/index.js index ae531a7..e5072cf 100644 --- a/yaba-web/src/components/index.js +++ b/yaba-web/src/components/index.js @@ -1,12 +1,13 @@ import { Footer } from "./footer"; import { Header } from "./header"; -import { Bookmark } from "./bookmark/bookmark"; +import { Bookmark } from "./bookmarks/bookmark"; 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"; +import { Tag } from "./tags/tag"; export { Footer, @@ -18,4 +19,5 @@ export { SearchForm, UpsertTagModal, InterceptModal, + Tag, }; \ No newline at end of file diff --git a/yaba-web/src/components/tags/tag.jsx b/yaba-web/src/components/tags/tag.jsx new file mode 100644 index 0000000..8709194 --- /dev/null +++ b/yaba-web/src/components/tags/tag.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Button } from "../external"; + +export function Tag(props) { + return ( + + ); +}; \ No newline at end of file diff --git a/yaba-web/src/views/bookmarksListView.jsx b/yaba-web/src/views/bookmarksListView.jsx index c2667bc..837e9d2 100644 --- a/yaba-web/src/views/bookmarksListView.jsx +++ b/yaba-web/src/views/bookmarksListView.jsx @@ -3,7 +3,7 @@ import { Alert, Col, Container, Row, Button, Modal, Dropdown, DropdownButton } f import { Bookmark } from "../components"; import { useAuth0 } from "@auth0/auth0-react"; import { deleteBookmarks, getAllBookmarks, hideBookmarks } from "../api"; -import { SplashScreen, SearchForm } from "../components"; +import { SplashScreen, SearchForm, Tag } from "../components"; import { getTagGroups, isSubset, containsSubstring, flattenTagArrays } from "../utils"; export function BookmarksListView(props) { @@ -334,11 +334,7 @@ export function BookmarksListView(props) { {group.name}
{ - group.tags.map((tag) => { - return - }) + group.tags.map((tag) => onTagSelected(true, tag)} />) }
}) -- 2.49.0 From 657000ddd6150ae94b424be49dbf7beb5506f1c3 Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Tue, 21 Feb 2023 23:04:52 -0600 Subject: [PATCH 5/8] Escaped HTML special characters and truncated descriptions to 100 GH-13 --- YABA.API/Settings/AutoMapperProfile.cs | 5 ++++- yaba-web/src/components/bookmarks/bookmark.jsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/YABA.API/Settings/AutoMapperProfile.cs b/YABA.API/Settings/AutoMapperProfile.cs index a721a3f..920b80a 100644 --- a/YABA.API/Settings/AutoMapperProfile.cs +++ b/YABA.API/Settings/AutoMapperProfile.cs @@ -1,4 +1,5 @@ using AutoMapper; +using System.Net; using YABA.API.ViewModels; using YABA.API.ViewModels.Bookmarks; using YABA.API.ViewModels.Tags; @@ -13,7 +14,9 @@ namespace YABA.API.Settings public AutoMapperProfile() { CreateMap(); - CreateMap(); + CreateMap() + .ForMember(dest => dest.Title, opt => opt.MapFrom(src => WebUtility.HtmlDecode(src.Title))) + .ForMember(dest => dest.Description, opt => opt.MapFrom(src => WebUtility.HtmlDecode(src.Description))); CreateMap(); CreateMap(); CreateMap(); diff --git a/yaba-web/src/components/bookmarks/bookmark.jsx b/yaba-web/src/components/bookmarks/bookmark.jsx index 09ef9a0..cc3c09d 100644 --- a/yaba-web/src/components/bookmarks/bookmark.jsx +++ b/yaba-web/src/components/bookmarks/bookmark.jsx @@ -15,7 +15,7 @@ export function Bookmark(props) { {props.bookmark.title} -
{props.bookmark.description.substring(0, 100)}
+
{`${props.bookmark.description.substring(0, 97)}${props.bookmark.description.length >= 97 ? "..." : ""}`}
{ props.bookmark.tags.map((tag) => ) -- 2.49.0 From 51ffab601578ea7b5aa8fc75a9370861441f8dc9 Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Wed, 22 Feb 2023 19:54:10 -0600 Subject: [PATCH 6/8] Modified Tag and Bookmark list pages to highlight text that matches searchstring --- .../src/components/bookmarks/bookmark.jsx | 12 ++++-- yaba-web/src/components/tags/tag.jsx | 5 ++- yaba-web/src/utils/index.js | 4 +- yaba-web/src/utils/stringsHelper.js | 10 +++++ yaba-web/src/views/bookmarksListView.jsx | 1 + yaba-web/src/views/tagsView.jsx | 39 +++++++------------ 6 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 yaba-web/src/utils/stringsHelper.js diff --git a/yaba-web/src/components/bookmarks/bookmark.jsx b/yaba-web/src/components/bookmarks/bookmark.jsx index cc3c09d..af392d9 100644 --- a/yaba-web/src/components/bookmarks/bookmark.jsx +++ b/yaba-web/src/components/bookmarks/bookmark.jsx @@ -1,24 +1,28 @@ import React from "react"; import { Row, Col, Form, Button } from "../external"; import DateTimeHelper from "../../utils/dateTimeHelper"; -import "../../styles/component/bookmark.css"; +import { getHighlightedText } from "../../utils"; import { useNavigate } from "react-router-dom"; import { Tag } from "../../components"; export function Bookmark(props) { const navigate = useNavigate(); + const getTruncatedDescription = () => `${props.bookmark.description.substring(0, 97)}${props.bookmark.description.length >= 97 ? "..." : ""}`; + const getHighlightedTitle = () => getHighlightedText(props.bookmark.title, props.searchString); + const getHighlightedDescription = () => getHighlightedText(getTruncatedDescription(), props.searchString) + return
{props.onBookmarkSelected(e.target.checked)}} checked={props.bookmark.isSelected} /> - {props.bookmark.title} -
{`${props.bookmark.description.substring(0, 97)}${props.bookmark.description.length >= 97 ? "..." : ""}`}
+ { getHighlightedTitle() } +
{ getHighlightedDescription()}
{ - props.bookmark.tags.map((tag) => ) + props.bookmark.tags.map((tag) => ) }
diff --git a/yaba-web/src/components/tags/tag.jsx b/yaba-web/src/components/tags/tag.jsx index 8709194..d24e993 100644 --- a/yaba-web/src/components/tags/tag.jsx +++ b/yaba-web/src/components/tags/tag.jsx @@ -1,13 +1,16 @@ import React from "react"; import { Button } from "../external"; +import { getHighlightedText } from "../../utils"; export function Tag(props) { + const getHighlightedTagName = () => props.searchString ? getHighlightedText(props.tag.name, props.searchString) : props.tag.name + return ( ); }; \ No newline at end of file diff --git a/yaba-web/src/utils/index.js b/yaba-web/src/utils/index.js index 523bb57..31cce7c 100644 --- a/yaba-web/src/utils/index.js +++ b/yaba-web/src/utils/index.js @@ -2,11 +2,13 @@ import { isDev } from "./isDevHelper"; import { getTagGroups, flattenTagArrays } from "./tagsHelper"; import { isSubset } from "./arrayHelper"; import { containsSubstring } from "./bookmarkHelper"; +import { getHighlightedText } from "./stringsHelper"; export { isDev, getTagGroups, isSubset, containsSubstring, - flattenTagArrays + flattenTagArrays, + getHighlightedText, } \ No newline at end of file diff --git a/yaba-web/src/utils/stringsHelper.js b/yaba-web/src/utils/stringsHelper.js new file mode 100644 index 0000000..ef17ab5 --- /dev/null +++ b/yaba-web/src/utils/stringsHelper.js @@ -0,0 +1,10 @@ +// https://stackoverflow.com/questions/29652862/highlight-text-using-reactjs +export const getHighlightedText = (text, highlight) => { + // Split on highlight term and include term into parts, ignore case + const parts = text.split(new RegExp(`(${highlight})`, 'gi')); + return { parts.map((part, i) => + + { part } + ) + } ; +} \ No newline at end of file diff --git a/yaba-web/src/views/bookmarksListView.jsx b/yaba-web/src/views/bookmarksListView.jsx index 837e9d2..18afe94 100644 --- a/yaba-web/src/views/bookmarksListView.jsx +++ b/yaba-web/src/views/bookmarksListView.jsx @@ -323,6 +323,7 @@ export function BookmarksListView(props) { onBookmarkSelected={(selected) => onBookmarkSelected(selected, bookmark)} onDeleteClicked={() => handleDeleteBookmark(bookmark.id)} onHideClicked={() => handleHideBookmarks([bookmark.id])} + searchString={searchString} /> }) } diff --git a/yaba-web/src/views/tagsView.jsx b/yaba-web/src/views/tagsView.jsx index f35f1cd..a4cf506 100644 --- a/yaba-web/src/views/tagsView.jsx +++ b/yaba-web/src/views/tagsView.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useReducer, useState } from "react"; import { useAuth0 } from "@auth0/auth0-react"; -import { containsSubstring } from "../utils"; +import { containsSubstring, getHighlightedText } 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"; @@ -9,24 +9,6 @@ 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 }; @@ -127,11 +109,7 @@ export function TagsView(props) { 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)})); + newState = state.map(x => ({...x, isDisplayed: !searchString || containsSubstring(x, searchString)})); break; case "TAG_UPDATE": newState = [...state]; @@ -314,6 +292,17 @@ export function TagsView(props) { } }; + const handleSearch = (e) => { + e.preventDefault(); + setSearchString(e.target[0].value); + dispatchTagsState({type: "SEARCH"}); + }; + + const handleSearchStringChange = (e) => { + setSearchString(e.target.value); + dispatchTagsState({type: "SEARCH"}); + }; + useEffect(() => { dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", payload: {message: "Retrieving Tags..."}}); fetchTags(); @@ -409,7 +398,7 @@ export function TagsView(props) { checked={tag.isSelected} /> - {tag.name} + {getHighlightedText(tag.name, searchString)} { tag.isHidden ? "Yes" : "No" } -- 2.49.0 From 9a221fcca110d69e9547bb752f848cdb2d46f592 Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Wed, 22 Feb 2023 21:10:24 -0600 Subject: [PATCH 7/8] Added hints under the tags field for which delimeters will be used --- yaba-web/src/views/bookmarkDetailView.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/yaba-web/src/views/bookmarkDetailView.jsx b/yaba-web/src/views/bookmarkDetailView.jsx index fa05ff1..c6a039a 100644 --- a/yaba-web/src/views/bookmarkDetailView.jsx +++ b/yaba-web/src/views/bookmarkDetailView.jsx @@ -106,7 +106,7 @@ export function BookmarkDetailView() { enableReinitialize: true, onSubmit: async(values) => { const isUpdate = values.id > 0 - values.tags = values.tags ? values.tags.split(", ") : null; + values.tags = values.tags ? values.tags.toLowerCase().split(/[\s,]+/) : null; dispatchAlertMessageState({ type: "CLOSE_ALERT"}); dispatchSplashScreenState({type: "SHOW_SPLASH_SCREEN", message: isUpdate ? "Updating Bookmark entry" : "Creating new Bookmark"}); const accessToken = await getAccessTokenSilently(); @@ -210,6 +210,9 @@ export function BookmarkDetailView() { defaultValue={formik.values.tags} onChange={formik.handleChange} /> + + Tags will be separated by commas or spaces + Title: @@ -220,7 +223,7 @@ export function BookmarkDetailView() { defaultValue={formik.values.title} onChange={formik.handleChange} /> - + When left blank, we'll use the URL's Title -- 2.49.0 From f298891572d5900ca1dbeddf3f100d9043eb77ee Mon Sep 17 00:00:00 2001 From: Carl Tibule Date: Thu, 23 Feb 2023 00:06:24 -0600 Subject: [PATCH 8/8] Modified register user service method to be async --- YABA.API/Controllers/UsersController.cs | 4 ++-- YABA.API/Middlewares/AddCustomClaimsMiddleware.cs | 2 -- YABA.Service/Interfaces/IUserService.cs | 9 +++++---- YABA.Service/UserService.cs | 5 +++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/YABA.API/Controllers/UsersController.cs b/YABA.API/Controllers/UsersController.cs index 0829682..2161c3b 100644 --- a/YABA.API/Controllers/UsersController.cs +++ b/YABA.API/Controllers/UsersController.cs @@ -27,7 +27,7 @@ namespace YABA.API.Controllers [ProducesResponseType(typeof(UserResponse), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NoContent)] - public IActionResult Register() + public async Task Register() { var authProviderId = this.GetAuthProviderId(); @@ -37,7 +37,7 @@ namespace YABA.API.Controllers if (isRegistered) return NoContent(); - var registedUser = _userService.RegisterUser(authProviderId); + var registedUser = await _userService.RegisterUser(authProviderId); return Ok(_mapper.Map(registedUser)); } } diff --git a/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs b/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs index f837fdc..db3893f 100644 --- a/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs +++ b/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs @@ -19,8 +19,6 @@ namespace YABA.API.Middlewares { if (httpContext.User != null && httpContext.User.Identity.IsAuthenticated) { - var claims = new List(); - var userAuthProviderId = httpContext.User.Identity.GetAuthProviderId(); if (!string.IsNullOrEmpty(userAuthProviderId)) diff --git a/YABA.Service/Interfaces/IUserService.cs b/YABA.Service/Interfaces/IUserService.cs index 6764766..95e3c45 100644 --- a/YABA.Service/Interfaces/IUserService.cs +++ b/YABA.Service/Interfaces/IUserService.cs @@ -1,11 +1,12 @@ -using YABA.Common.DTOs; +using System.Threading.Tasks; +using YABA.Common.DTOs; namespace YABA.Service.Interfaces { public interface IUserService { - public bool IsUserRegistered(string authProviderId); - public UserDTO RegisterUser(string authProviderId); - public int GetUserId(string authProviderId); + bool IsUserRegistered(string authProviderId); + Task RegisterUser(string authProviderId); + int GetUserId(string authProviderId); } } diff --git a/YABA.Service/UserService.cs b/YABA.Service/UserService.cs index 3269525..534cd52 100644 --- a/YABA.Service/UserService.cs +++ b/YABA.Service/UserService.cs @@ -1,5 +1,6 @@ using AutoMapper; using System.Linq; +using System.Threading.Tasks; using YABA.Common.DTOs; using YABA.Data.Context; using YABA.Models; @@ -25,7 +26,7 @@ namespace YABA.Service return _roContext.Users.Any(x => x.Auth0Id == authProviderId); } - public UserDTO RegisterUser(string authProviderId) + public async Task RegisterUser(string authProviderId) { if(IsUserRegistered(authProviderId)) { @@ -39,7 +40,7 @@ namespace YABA.Service }; var registedUser = _context.Users.Add(userToRegister); - return _context.SaveChanges() > 0 ? _mapper.Map(registedUser.Entity) : null; + return await _context.SaveChangesAsync() > 0 ? _mapper.Map(registedUser.Entity) : null; } public int GetUserId(string authProviderId) -- 2.49.0