diff --git a/src/main/java/io/hexlet/typoreporter/repository/TypoRepository.java b/src/main/java/io/hexlet/typoreporter/repository/TypoRepository.java index 265c7db6..4ab33bb7 100644 --- a/src/main/java/io/hexlet/typoreporter/repository/TypoRepository.java +++ b/src/main/java/io/hexlet/typoreporter/repository/TypoRepository.java @@ -17,6 +17,8 @@ public interface TypoRepository extends JpaRepository { Page findPageTypoByWorkspaceName(Pageable pageable, String name); + Page findPageTypoByWorkspaceNameAndTypoStatus(Pageable pageable, String name, TypoStatus status); + Integer deleteTypoById(Long id); @Query(""" diff --git a/src/main/java/io/hexlet/typoreporter/service/TypoService.java b/src/main/java/io/hexlet/typoreporter/service/TypoService.java index 9612153c..998e5925 100644 --- a/src/main/java/io/hexlet/typoreporter/service/TypoService.java +++ b/src/main/java/io/hexlet/typoreporter/service/TypoService.java @@ -70,6 +70,22 @@ public Page getTypoPage(final Pageable pageable, final String wksName) .map(typoMapper::toTypoInfo); } + @Transactional(readOnly = true) + public Page getTypoPageFiltered(final Pageable pageable, final String wksName, final String typoStatus) { + if (!workspaceService.existsWorkspaceByName(wksName)) { + throw new WorkspaceNotFoundException(wksName); + } + + TypoStatus typoStatusEnum; + try { + typoStatusEnum = TypoStatus.valueOf(typoStatus); + } catch (IllegalArgumentException e) { + typoStatusEnum = null; + } + + return repository.findPageTypoByWorkspaceNameAndTypoStatus(pageable, wksName, typoStatusEnum) + .map(typoMapper::toTypoInfo); + } @Transactional(readOnly = true) public List> getCountTypoByStatusForWorkspaceName(final String wksName) { diff --git a/src/main/java/io/hexlet/typoreporter/web/WorkspaceController.java b/src/main/java/io/hexlet/typoreporter/web/WorkspaceController.java index 333daedb..bf25378e 100644 --- a/src/main/java/io/hexlet/typoreporter/web/WorkspaceController.java +++ b/src/main/java/io/hexlet/typoreporter/web/WorkspaceController.java @@ -1,6 +1,7 @@ package io.hexlet.typoreporter.web; import io.hexlet.typoreporter.domain.account.Account; +import io.hexlet.typoreporter.domain.typo.TypoStatus; import io.hexlet.typoreporter.domain.workspace.Workspace; import io.hexlet.typoreporter.domain.workspace.WorkspaceRole; import io.hexlet.typoreporter.service.AccountService; @@ -40,6 +41,7 @@ import org.springframework.web.bind.annotation.RequestParam; import java.security.Principal; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -65,6 +67,11 @@ public class WorkspaceController { private final TreeSet availableSizes = new TreeSet<>(List.of(2, 5, 10, 15, 25)); + private final List availableStatuses = Arrays.asList(TypoStatus.values()); +// .stream() +// .map(status -> status.name().replace("_"," ")) +// .collect(Collectors.toList()); + private final TypoService typoService; private final WorkspaceService workspaceService; @@ -121,6 +128,7 @@ public String getWorkspaceInfoPage(Model model, @PathVariable String wksName) { @PreAuthorize(IS_USER_RELATED_TO_WKS) public String getWorkspaceTyposPage(Model model, @PathVariable String wksName, + @RequestParam(required = false) String typoStatus, @SortDefault("createdDate") Pageable pageable) { var wksOptional = workspaceService.getWorkspaceInfoByName(wksName); if (wksOptional.isEmpty()) { @@ -128,14 +136,22 @@ public String getWorkspaceTyposPage(Model model, log.error("Workspace with name {} not found", wksName); return "redirect:/workspaces"; } + model.addAttribute("wksName", wksName); model.addAttribute("wksInfo", wksOptional.get()); getStatisticDataToModel(model, wksName); getLastTypoDataToModel(model, wksName); - var size = Optional.ofNullable(availableSizes.floor(pageable.getPageSize())).orElseGet(availableSizes::first); + var size = Optional.ofNullable(availableSizes.floor(pageable.getPageSize())).orElseGet(availableSizes::first); var pageRequest = PageRequest.of(pageable.getPageNumber(), size, pageable.getSort()); - var typoPage = typoService.getTypoPage(pageRequest, wksName); + Page typoPage; + if (typoStatus == null) { + typoPage = typoService.getTypoPage(pageRequest, wksName); + } else { + typoPage = typoService.getTypoPageFiltered(pageRequest, wksName, typoStatus); + model.addAttribute("typoStatus", typoStatus); + model.addAttribute("typoStatusStyle", TypoStatus.valueOf(typoStatus).getStyle()); + } var sort = typoPage.getSort() .stream() @@ -144,6 +160,7 @@ public String getWorkspaceTyposPage(Model model, model.addAttribute("typoPage", typoPage); model.addAttribute("availableSizes", availableSizes); + model.addAttribute("availableStatuses", availableStatuses); model.addAttribute("sortProp", sort.getProperty()); model.addAttribute("sortDir", sort.getDirection()); model.addAttribute("DESC", DESC); diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 700f0bed..59a98b88 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -22,6 +22,12 @@ RESOLVED=RESOLVED CANCELED=CANCELED TOTAL=TOTAL +# TYPO EVENTS +OPEN=OPEN +RESOLVE=RESOLVE +REOPEN=REOPEN +CANCEL=CANCEL + btn.create-account=Create Account btn.save=Save btn.close=Close @@ -64,6 +70,7 @@ alert.report-success=Error message sent successfully. Thank you! btn.send=Send btn.regenerate-token=Regenerate Token btn.page-size=Page size +btn.page-empty-filter=All statuses text.leave-comment=Leave additional comment: text.last-typo-added=Last typo added {0} at {1} @@ -75,6 +82,9 @@ text.script-descr=Install this script on your website: text.modified-info=Modified by {0} {1} at {2} text.created-info=Created by {0} {1} at {2} +text.created-at=Created at +text.modified-at=Modified at +text.reported-by=Reported by text.add-user-to-wks=Adding a user to a workspace text.confirm-delete-user-wks=Are you sure you want to delete user from this workspace? text.hint-choose-a-user=Choose a user from the list diff --git a/src/main/resources/messages_ru.properties b/src/main/resources/messages_ru.properties index 49b4e5ee..e5e5a3d6 100644 --- a/src/main/resources/messages_ru.properties +++ b/src/main/resources/messages_ru.properties @@ -22,12 +22,19 @@ RESOLVED=РЕШЕНО CANCELED=ОТМЕНЕНО TOTAL=ВСЕГО +# TYPO EVENTS +OPEN=ОТКРЫТЬ +RESOLVE=ВЫПОЛНИТЬ +REOPEN=ОТКРЫТЬ ЗАНОВО +CANCEL=ОТМЕНИТЬ + btn.edit=Изменить btn.send=Отправить btn.save=Сохранить btn.close=Закрыть btn.create=Создать btn.page-size=Размер страницы +btn.page-empty-filter=Все статусы btn.add-to-wks=Добавить в пространство btn.delete-from-wks=Удалить из пространства btn.create-account=Создать Аккаунт @@ -73,6 +80,9 @@ text.use-header=Используйте заголовок text.script-descr=Установите этот скрипт на ваш сайт: text.modified-info=Изменен пользователем {0} {1} в {2} text.created-info=Создан пользователем {0} {1} в {2} +text.created-at=Создан +text.modified-at=Изменен +text.reported-by=Отправитель text.add-user-to-wks=Добавить пользователя в пространство text.confirm-delete-user-wks=Вы действительно хотите удалить пользователя из рабочего пространства? text.hint-choose-a-user=Выберите пользователя из списка diff --git a/src/main/resources/templates/fragments/panels.html b/src/main/resources/templates/fragments/panels.html index b5085e4b..7e8edfa7 100644 --- a/src/main/resources/templates/fragments/panels.html +++ b/src/main/resources/templates/fragments/panels.html @@ -65,7 +65,7 @@
- + + + + + +
    @@ -59,37 +93,49 @@ + th:href="${typoStatus} != null ? + @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|pageUrl,${sortProp == 'pageUrl' && sortDir.isAscending() ? DESC : ASC}|, typoStatus=${typoStatus})} + : @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|pageUrl,${sortProp == 'pageUrl' && sortDir.isAscending() ? DESC : ASC}|)}"> [[${sortProp == 'pageUrl' && sortDir.isAscending() ? '⇑' : '⇓'}]] + th:href="${typoStatus} != null ? + @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|reporterName,${sortProp == 'reporterName' && sortDir.isAscending() ? DESC : ASC}|, typoStatus=${typoStatus})} + : @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|reporterName,${sortProp == 'reporterName' && sortDir.isAscending() ? DESC : ASC}|)}"> [[${sortProp == 'reporterName' && sortDir.isAscending() ? '⇑' : '⇓'}]] + th:href="${typoStatus} != null ? + @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|createdDate,${sortProp == 'createdDate' && sortDir.isAscending() ? DESC : ASC}|, typoStatus=${typoStatus})} + : @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|createdDate,${sortProp == 'createdDate' && sortDir.isAscending() ? DESC : ASC}|)}"> [[${sortProp == 'createdDate' && sortDir.isAscending() ? '⇑' : '⇓'}]] + th:href="${typoStatus} != null ? + @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|modifiedDate,${sortProp == 'modifiedDate' && sortDir.isAscending() ? DESC : ASC}|, typoStatus=${typoStatus})} + : @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|modifiedDate,${sortProp == 'modifiedDate' && sortDir.isAscending() ? DESC : ASC}|)}"> [[${sortProp == 'modifiedDate' && sortDir.isAscending() ? '⇑' : '⇓'}]] + th:href="${typoStatus} != null ? + @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|modifiedBy,${sortProp == 'modifiedBy' && sortDir.isAscending() ? DESC : ASC}|, typoStatus=${typoStatus})} + : @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|modifiedBy,${sortProp == 'modifiedBy' && sortDir.isAscending() ? DESC : ASC}|)}"> [[${sortProp == 'modifiedBy' && sortDir.isAscending() ? '⇑' : '⇓'}]] + th:href="${typoStatus} != null ? + @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|typoStatus,${sortProp == 'typoStatus' && sortDir.isAscending() ? DESC : ASC}|, typoStatus=${typoStatus})} + : @{'/workspace/' + ${wksName} + '/typos'(size=*{size},sort=|typoStatus,${sortProp == 'typoStatus' && sortDir.isAscending() ? DESC : ASC}|)}"> [[${sortProp == 'typoStatus' && sortDir.isAscending() ? '⇑' : '⇓'}]] @@ -107,7 +153,7 @@ - + @@ -126,7 +172,8 @@ th:method="patch">
    @@ -135,7 +182,7 @@
    -
    @@ -143,11 +190,11 @@
    -

    -

    +

    +


    + th:text="|#{text.reported-by} *{reporterName}: *{reporterComment}|">


    [[*{textBeforeTypo}]][[*{textTypo}]][[*{textAfterTypo}]] diff --git a/src/main/resources/templates/workspaces.html b/src/main/resources/templates/workspaces.html index 30b0c3c4..594ed9e9 100644 --- a/src/main/resources/templates/workspaces.html +++ b/src/main/resources/templates/workspaces.html @@ -13,7 +13,7 @@

    diff --git a/src/test/java/io/hexlet/typoreporter/test/factory/EntitiesFactory.java b/src/test/java/io/hexlet/typoreporter/test/factory/EntitiesFactory.java index da177e4a..53c13d83 100644 --- a/src/test/java/io/hexlet/typoreporter/test/factory/EntitiesFactory.java +++ b/src/test/java/io/hexlet/typoreporter/test/factory/EntitiesFactory.java @@ -59,6 +59,15 @@ public static Stream getWorkspacesAndUsersRelated() { ); } + public static Stream getWorkspacesAndUsersAndTypoStatusRelated() { + return Stream.of( + Arguments.of(WORKSPACE_102_NAME, ACCOUNT_102_USERNAME, "REPORTED"), + Arguments.of(WORKSPACE_102_NAME, ACCOUNT_102_USERNAME, "IN_PROGRESS"), + Arguments.of(WORKSPACE_102_NAME, ACCOUNT_102_USERNAME, "RESOLVED"), + Arguments.of(WORKSPACE_102_NAME, ACCOUNT_102_USERNAME, "CANCELED") + ); + } + public static Stream getWorkspacesAndUsersNotRelated() { return Stream.of( Arguments.of(WORKSPACE_101_NAME, ACCOUNT_103_USERNAME), diff --git a/src/test/java/io/hexlet/typoreporter/web/WorkspaceControllerIT.java b/src/test/java/io/hexlet/typoreporter/web/WorkspaceControllerIT.java index b86c256e..5691a5c5 100644 --- a/src/test/java/io/hexlet/typoreporter/web/WorkspaceControllerIT.java +++ b/src/test/java/io/hexlet/typoreporter/web/WorkspaceControllerIT.java @@ -6,6 +6,7 @@ import io.hexlet.typoreporter.domain.AbstractAuditingEntity; import io.hexlet.typoreporter.domain.account.Account; import io.hexlet.typoreporter.domain.typo.Typo; +import io.hexlet.typoreporter.domain.typo.TypoStatus; import io.hexlet.typoreporter.domain.workspace.AccountRole; import io.hexlet.typoreporter.domain.workspace.Workspace; import io.hexlet.typoreporter.domain.workspace.WorkspaceRoleId; @@ -40,6 +41,7 @@ import static io.hexlet.typoreporter.test.Constraints.POSTGRES_IMAGE; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -56,7 +58,7 @@ @Transactional @DBRider @DBUnit(caseInsensitiveStrategy = LOWERCASE, dataTypeFactoryClass = DBUnitEnumPostgres.class, cacheConnection = false) -@DataSet(value = {"workspaces.yml", "workspaceRoles.yml", "accounts.yml"}) +@DataSet(value = {"workspaces.yml", "workspaceRoles.yml", "accounts.yml", "typos.yml"}) class WorkspaceControllerIT { @Container @@ -151,6 +153,31 @@ void getWorkspaceTyposPageIsSuccessful(final String wksName, final String userna } } + @ParameterizedTest + @MethodSource("io.hexlet.typoreporter.test.factory.EntitiesFactory#getWorkspacesAndUsersAndTypoStatusRelated") + void getWorkspaceTyposPageFilteredIsSuccessful(final String wksName, final String username, final String typoStatus) throws Exception { + Workspace workspace = repository.getWorkspaceByName(wksName).orElse(null); + + var request = get("/workspace/{wksName}/typos", wksName).queryParam("typoStatus", typoStatus); + + MockHttpServletResponse response = mockMvc.perform(request + .with(user(username))) + .andExpect(model().attributeExists("wksInfo", "wksName", "typoPage", "availableSizes", "sortProp", "sortDir", "DESC", "ASC", "typoStatus")) + .andReturn().getResponse(); + + Typo typo = workspace.getTypos().stream() + .filter(t -> !t.getTypoStatus().equals(TypoStatus.valueOf(typoStatus))) + .findFirst().orElse(null); + + if (typo != null) { + assertThat(response.getContentAsString()).contains( + typo.getPageUrl(), typo.getReporterName(), typo.getModifiedBy() + ); + } else { + fail("No typos without status " + typoStatus); + } + } + @Test void getWorkspaceSettingsPageWithoutWksInfo() throws Exception { mockMvc.perform(get("/workspace/{wksName}/typos", "notExistsWksName"))