Skip to content

Commit

Permalink
✨ 53 feature pagination for ad list (#95)
Browse files Browse the repository at this point in the history
* removed delete button

* 🍺 edited api spec

* generated api specs

* replaced enum location with new location

* 🍺

* 💩 updated repository implementation - needs correction

* ♻️ removed old method and introduced sorting/ordering

* 🐛 empty query upon empty input

* addes route for the users board / myboard

* added routing to accountcard to myboard

* 🐛 renamed query param

* added eventbus for updating adList

* added logic to list to get list of ads from backend

* adjusted cards to show information from api

* 🐛 spelling mistake

* updated links and eventbus

* use eventbus to trigger update

* added delay to request update

* 💩 introduced composable to detect myboard

* added route param as injected value

* Minor spelling and redirect changes

* removed composable

* introduced store for ads

* prevents user from loading if exists

* prevents categories from loading if exists

* ✨ added ability to remove selected user

* 🐛 reset querys with null

* 💬 spelling mistake

* computed the chip value

* update logic with watcher

* used computed prop for myboard

* 🐛 userId is not a string

* removed props from myboard route

* adapted icon text to display a link

* removed prop isMyboard

* added empty note

* 💄 added loading spinner for ad-details

* added default querys

* 🚨 added type to function parameter

* added constants for queries and changed router links

* replaced query names with constants

* 🚨 fixed linting

* 🚨 fixed pmd issues

* 🚨 spotless ... grr

---------

Co-authored-by: jannik.lange <[email protected]>
Co-authored-by: langehm <[email protected]>
  • Loading branch information
3 people authored Jan 9, 2025
1 parent 472ac57 commit 0c37db2
Show file tree
Hide file tree
Showing 36 changed files with 794 additions and 319 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Page<AdTO> searchActiveAds(String userId,
Pageable pageable,
Long adId);

@PreAuthorize("hasAuthority(T(de.muenchen.intranet.sbrett.security.AuthoritiesEnum).BACKEND_READ_THEENTITY.name())")
@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_READ_THEENTITY.name())")
Page<AdTO> searchDeactivatedAds(String userId,
String searchTerm,
Long categoryId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.security.access.prepost.PreAuthorize;
Expand All @@ -18,6 +19,7 @@
import java.util.Locale;
import java.util.stream.Collectors;

@Slf4j
@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.UselessParentheses" })
@Repository
public class AdRepositoryCustomImpl implements AdRepositoryCustom {
Expand All @@ -43,102 +45,86 @@ public Page<AdTO> searchActiveAds(final String userId, final String searchTerm,

@Override
@SuppressWarnings({ "PMD.UseObjectWithCaseConventions", "PMD.UseObjectForClearerAPI" })
@PreAuthorize("hasAuthority(T(de.muenchen.intranet.sbrett.security.AuthoritiesEnum).BACKEND_READ_THEENTITY.name())")
@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_READ_THEENTITY.name())")
public Page<AdTO> searchDeactivatedAds(final String userId, final String searchTerm, final Long categoryId, final AdType type, final String sortBy,
final String order, final Pageable pageable,
final Long adId) {
return searchAds(userId, searchTerm, categoryId, type, sortBy, order, pageable, adId, false);
}

@SuppressWarnings("PMD.UseObjectForClearerAPI")
@SuppressWarnings({ "PMD.UseObjectForClearerAPI" })
public Page<AdTO> searchAds(final String userId, final String searchTerm, final Long categoryId, final AdType type, final String sortBy, final String order,
final Pageable pageable, final Long adId,
final boolean isActive) {
final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Ad> query = builder.createQuery(Ad.class);

final Root<Ad> root = query.from(Ad.class);
final Path<Long> pathSwbUserId = root.get("swbUser").get("id");
final Path<Long> pathCategoryId = root.get("adCategory").get("id");
final Path<AdType> pathAdType = root.get("adType");
final Path<Boolean> pathActive = root.get("active");
final Path<String> pathTitle = root.get("title");
final Path<String> pathDescription = root.get("description");
final Path<Double> pathAdId = root.get("id");

/**
* Search and Filter
*/
final List<Predicate> predicates = new ArrayList<>();

// active = false ist für den User wie gelöscht
final Predicate filterActive = builder.equal(pathActive, isActive);
predicates.add(filterActive);
final Pageable pageable, final Long adId, final boolean isActive) {

if (userId != null) {
final Predicate filterUser = builder.equal(pathSwbUserId, userId);
predicates.add(filterUser);
}
if (categoryId != null) {
final Predicate filterCategory = builder.equal(pathCategoryId, categoryId);
predicates.add(filterCategory);
}
if (type != null) {
final Predicate filterType = builder.equal(pathAdType, type);
predicates.add(filterType);
}
if (searchTerm != null) {
final Predicate searchTitle = builder.like(builder.lower(pathTitle), "%" + searchTerm.toLowerCase(Locale.GERMAN) + "%");
final Predicate searchDescription = builder.like(builder.lower(pathDescription), "%" + searchTerm.toLowerCase(Locale.GERMAN) + "%");
predicates.add(builder.or(searchTitle, searchDescription));
}
if (adId != null) {
final Predicate filterAdId = builder.equal(pathAdId, adId);
predicates.add(filterAdId);
}
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();

final Predicate[] finalPredicates = predicates.toArray(new Predicate[predicates.size()]);
// Main query
final CriteriaQuery<Ad> cq = cb.createQuery(Ad.class);
final Root<Ad> ad = cq.from(Ad.class);
final List<Predicate> predicates = buildPredicates(cb, ad, isActive, userId, searchTerm, categoryId, type, adId);
cq.where(predicates.toArray(new Predicate[0]));

query.select(root);
query.where(builder.and(finalPredicates));
// Add sorting and order
final Order orderCriteria = buildSorter(cb, ad, sortBy, order);
cq.orderBy(orderCriteria);

/**
* Sort and Order
*/
Expression<Object> sortExpression = root.get(sortBy);
final TypedQuery<Ad> query = entityManager.createQuery(cq);

if (PRICE_STRING.equals(sortBy)) {
// Apply pagination
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());

// Sonderbehandlung bei Sortierung nach Preis.
final Expression<Integer> se2 = root.get(sortBy);
sortExpression = (builder.selectCase()
.when(builder.equal(sortExpression, 0), -1) // 0: Zu verschenken
.when(builder.greaterThan(se2, 0), sortExpression) // >0: Festpreis
.when(builder.lessThan(se2, 0), builder.neg(se2)) // <0: Verhandelbar
);
}
final List<AdTO> ads = query.getResultList()
.stream()
.map(mapper::toAdTO)
.collect(Collectors.toList());

if (ORDER_ASC.equals(order)) {
query.orderBy(builder.asc(sortExpression));
} else if (ORDER_DESC.equals(order)) {
query.orderBy(builder.desc(sortExpression));
}
// Count query for total elements
final CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
final Root<Ad> countRoot = countQuery.from(Ad.class);
final List<Predicate> countPredicates = buildPredicates(cb, countRoot, isActive, userId, searchTerm, categoryId, type, adId);
countQuery.select(cb.count(countRoot)).where(countPredicates.toArray(new Predicate[0]));
final Long total = entityManager.createQuery(countQuery).getSingleResult();

/**
* Pagination
*/
final TypedQuery<Ad> adsQuery = entityManager.createQuery(query);
adsQuery.setFirstResult((int) pageable.getOffset());
adsQuery.setMaxResults(pageable.getPageSize());
return new PageImpl<>(ads, pageable, total);
}

final List<AdTO> resultList = adsQuery.getResultList().stream().map(mapper::toAdTO).collect(Collectors.toList());
public Order buildSorter(final CriteriaBuilder cb, final Root<Ad> root, final String sortBy, final String order) {
// Add Sorting
final Expression<?> sortExpression = PRICE_STRING.equals(sortBy)
? cb.selectCase()
.when(cb.equal(root.get(sortBy), 0), -1) // 0: Zu verschenken
.when(cb.greaterThan(root.get(sortBy), 0), root.get(sortBy)) // >0: Festpreis
.otherwise(cb.neg(root.get(sortBy))) // <0: Verhandelbar
: root.get(sortBy);

// Add Ordering
return ORDER_ASC.equals(order) ? cb.asc(sortExpression) : cb.desc(sortExpression);
}

final CriteriaQuery<Long> countQuery = builder.createQuery(Long.class);
countQuery.where(builder.and(finalPredicates));
countQuery.select(builder.count(countQuery.from(Ad.class)));
private List<Predicate> buildPredicates(final CriteriaBuilder cb, final Root<Ad> root, final boolean isActive, final String userId, final String searchTerm,
final Long categoryId, final AdType type, final Long adId) {
final List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("active"), isActive));

final Long totalRows = entityManager.createQuery(countQuery).getSingleResult();
if (userId != null) {
predicates.add(cb.equal(root.get("swbUser").get("id"), userId));
}
if (searchTerm != null) {
final Predicate titlePredicate = cb.like(cb.lower(root.get("title")), "%" + searchTerm.toLowerCase(Locale.GERMAN) + "%");
final Predicate descriptionPredicate = cb.like(cb.lower(root.get("description")), "%" + searchTerm.toLowerCase(Locale.GERMAN) + "%");
predicates.add(cb.or(titlePredicate, descriptionPredicate));
}
if (categoryId != null) {
predicates.add(cb.equal(root.get("adCategory").get("id"), categoryId));
}
if (type != null) {
predicates.add(cb.equal(root.get("adType"), type));
}
if (adId != null) {
predicates.add(cb.equal(root.get("id"), adId));
}

return new PageImpl<>(resultList, pageable, totalRows);
return predicates;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public AdCategory createAdCategory(final AdCategory adCategory) {
return repository.save(adCategory);
}

@PreAuthorize("hasAuthority(T(de.muenchen.intranet.sbrett.security.AuthoritiesEnum).BACKEND_WRITE_THEENTITY.name())")
@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_WRITE_THEENTITY.name())")
public AdCategory saveAdCategory(final AdCategory adCategory) {
if (adCategory.isStandard()) {
getAdCategories().stream().forEach(cat -> {
Expand All @@ -40,7 +40,7 @@ public AdCategory saveAdCategory(final AdCategory adCategory) {
return repository.save(adCategory);
}

@PreAuthorize("hasAuthority(T(de.muenchen.intranet.sbrett.security.AuthoritiesEnum).BACKEND_DELETE_THEENTITY.name())")
@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_DELETE_THEENTITY.name())")
public void deleteAdCategory(final long id) {
final AdCategory category = repository.getOne(id);
final AdCategory standardCat = repository.findByStandardTrue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,13 @@ public void deactivateAd(final long id, HttpServletRequest request) {
repository.save(ad);
}

@PreAuthorize("hasAuthority(T(de.muenchen.intranet.sbrett.security.AuthoritiesEnum).BACKEND_DELETE_THEENTITY.name())")
@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_DELETE_THEENTITY.name())")
public void deleteAd(final long id, HttpServletRequest request) {
final Ad ad = repository.getOne(id);
repository.delete(ad);
}

@PreAuthorize("hasAuthority(T(de.muenchen.intranet.sbrett.security.AuthoritiesEnum).BACKEND_WRITE_THEENTITY.name())")
@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_WRITE_THEENTITY.name())")
public void updateAllCategories(final AdCategory oldCat, final AdCategory newCat) {
final List<Ad> allAdsOfCategory = repository.findByAdCategory(oldCat);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public SettingTO createSetting(final SettingTO settingTO) {
return mapper.toSettingTO(savedSetting);
}

@PreAuthorize("hasAuthority(T(de.muenchen.intranet.sbrett.security.AuthoritiesEnum).BACKEND_WRITE_THEENTITY.name())")
@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_WRITE_THEENTITY.name())")
public List<SettingTO> saveSettings(final List<SettingTO> settingTOs) {
final List<Setting> settings = settingTOs.stream().map(mapper::toSetting).collect(Collectors.toList());
final List<Setting> savedSettings = repository.saveAll(settings);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public SwbUser saveOrGet(final SwbUser user) {
return existingUser.orElse(repository.save(user));
}

@PreAuthorize("hasAuthority(T(de.muenchen.intranet.sbrett.security.AuthoritiesEnum).BACKEND_READ_THEENTITY.name())")
@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_READ_THEENTITY.name())")
public SwbUserTO getUser(final long id) {
final SwbUser user = repository.getOne(id);
return mapper.toSwbUserTO(user);
Expand Down
8 changes: 7 additions & 1 deletion anzeigen-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
cols="3"
class="d-flex align-center justify-start"
>
<router-link to="/">
<router-link
:to="{
name: ROUTES_BOARD,
query: DEFAULT_BOARD_QUERIES,
}"
>
<v-toolbar-title class="font-weight-bold">
<span class="text-white">Schwarzes-</span>
<span class="text-secondary">Brett</span>
Expand Down Expand Up @@ -66,6 +71,7 @@ import Ad2ImageAvatar from "@/components/common/Ad2ImageAvatar.vue";
import SearchAd from "@/components/Filter/SearchAd.vue";
import TheSnackbarQueue from "@/components/TheSnackbarQueue.vue";
import { useApi } from "@/composables/useApi";
import { DEFAULT_BOARD_QUERIES, ROUTES_BOARD } from "@/Constants";
import { useUserStore } from "@/stores/user";
useApi();
Expand Down
20 changes: 20 additions & 0 deletions anzeigen-frontend/src/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { InjectionKey, Ref } from "vue";

export const ROUTES_BOARD = "board";
export const ROUTES_MYBOARD = "myboard";
export const ROUTES_AD = "ad";
export const ROUTES_GETSTARTED = "getstarted";

Expand All @@ -13,15 +16,32 @@ export const ROUTER_TYPE = "hash";
export const EV_EDIT_AD_DIALOG = "eventbus-dialog";
export const EV_SNACKBAR = "eventbus-snackbar";
export const EV_CLEAR_CACHE = "eventBus-clear-cache";
export const EV_UPDATE_AD_LIST = "eventBus-update-ad-list";

/**
* Messages
*/
export const API_ERROR_MSG =
"Ein Fehler ist aufgetreten. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.";

/**
* Injection Keys
*/
export const IK_IS_MYBOARD: InjectionKey<Readonly<Ref<boolean>>> = Symbol(
"injection-key-my-board"
);

/**
* Other constants
*/
export const AD_MAX_TITLE_LENGTH = 40;
export const DATE_DISPLAY_FORMAT = "DD.MM.YYYY"; // use this in conjunction with useDateFormat
export const DEFAULT_BOARD_QUERIES = {
sortBy: "title",
order: "asc",
};
export const QUERY_NAME_ORDER = "order";
export const QUERY_NAME_SORTBY = "sortBy";
export const QUERY_NAME_TYPE = "type";
export const QUERY_NAME_CATEGORYID = "categoryId";
export const QUERY_NAME_USERID = "userId";
2 changes: 2 additions & 0 deletions anzeigen-frontend/src/api/swbrett/.openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ apis/index.ts
index.ts
models/AdCategory.ts
models/AdTO.ts
models/GetAds200Response.ts
models/GetAds200ResponseSort.ts
models/SettingTO.ts
models/SwbFileTO.ts
models/SwbImageTO.ts
Expand Down
9 changes: 6 additions & 3 deletions anzeigen-frontend/src/api/swbrett/apis/DefaultApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import type {
AdCategory,
AdTO,
GetAds200Response,
SettingTO,
SwbFileTO,
SwbImageTO,
Expand All @@ -25,6 +26,8 @@ import {
AdCategoryToJSON,
AdTOFromJSON,
AdTOToJSON,
GetAds200ResponseFromJSON,
GetAds200ResponseToJSON,
SettingTOFromJSON,
SettingTOToJSON,
SwbFileTOFromJSON,
Expand Down Expand Up @@ -727,7 +730,7 @@ export class DefaultApi extends runtime.BaseAPI {
async getAdsRaw(
requestParameters: GetAdsRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction
): Promise<runtime.ApiResponse<AdTO>> {
): Promise<runtime.ApiResponse<GetAds200Response>> {
if (requestParameters["isActive"] == null) {
throw new runtime.RequiredError(
"isActive",
Expand Down Expand Up @@ -786,7 +789,7 @@ export class DefaultApi extends runtime.BaseAPI {
);

return new runtime.JSONApiResponse(response, (jsonValue) =>
AdTOFromJSON(jsonValue)
GetAds200ResponseFromJSON(jsonValue)
);
}

Expand All @@ -796,7 +799,7 @@ export class DefaultApi extends runtime.BaseAPI {
async getAds(
requestParameters: GetAdsRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction
): Promise<AdTO> {
): Promise<GetAds200Response> {
const response = await this.getAdsRaw(requestParameters, initOverrides);
return await response.value();
}
Expand Down
Loading

0 comments on commit 0c37db2

Please sign in to comment.