From faa4c936d01fbc921ab698742e1b7c59fc10adab Mon Sep 17 00:00:00 2001 From: Vladimir Valekzhanin Date: Fri, 16 Oct 2026 12:10:14 +0300 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81=D0=BE=D0=B2=20=D1=81=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=B2=20=E2=84=962/=E2=84=963?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pages.md | 6 +- .../ResourceDetailsController.java | 30 ++++++ .../controllers/ResourceListController.java | 97 +++++++++++++++++-- src/main/resources/fxml/ResourceDetails.fxml | 4 + src/main/resources/fxml/ResourceList.fxml | 9 ++ 5 files changed, 134 insertions(+), 12 deletions(-) diff --git a/docs/pages.md b/docs/pages.md index a130d9c..7fc2e8e 100644 --- a/docs/pages.md +++ b/docs/pages.md @@ -28,9 +28,9 @@ UI не ходит в HTTP напрямую. Контроллер вызывае ### №2 — Список ресурсов -* Есть: `ResourceListController` + `ResourceList.fxml`: поле поиска по `title`, `ComboBox` сортировки (4 опции: `createdAt desc/asc`, `title asc/desc`), таблица (`title, description, createdAt, updatedAt`), пагинация `offset/count` с `PAGE_SIZE=20`, переход дабл-кликом в №3 через `Navigator.goTo(path, c -> setResourceId(uuid))`, кнопка «+ Создать документ» только для `MODER/ADMIN`. -* Нет: поля фильтра по `description` в UI нет (`ResourceFilter.description` всегда `null`); кнопки удаления в списке нет. -* Готово когда: заведено поле `description` (или зафиксировано решение «искать только по title») и заведена кнопка удаления с подтверждением (см. DOD №4). +* Есть: `ResourceListController` + `ResourceList.fxml`: поля поиска по `title` и `description`, `ComboBox` сортировки (4 опции: `createdAt desc/asc`, `title asc/desc`), таблица (`title, description, createdAt, updatedAt`), пагинация `offset/count` с `PAGE_SIZE=20`, переход дабл-кликом в №3 через `Navigator.goTo(path, c -> setResourceId(uuid))`, кнопка «+ Создать документ» и кнопка «Удалить» (по выбранной строке, с диалогом подтверждения) только для `MODER/ADMIN`. +* Нет: — +* Готово когда: закрыто — поиск по `description` заведён, удаление с подтверждением заведено (см. DOD №4). ### №3 — Просмотр метаданных ресурса diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java index ff24f32..36b30ca 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceDetailsController.java @@ -17,7 +17,9 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Pos; +import javafx.scene.control.Alert; import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; @@ -44,6 +46,7 @@ public class ResourceDetailsController extends Controller { @FXML TableColumn downloadColumn; @FXML Button backButton; @FXML Button editButton; + @FXML Button deleteButton; private UUID resourceId; @@ -88,6 +91,8 @@ protected void updateItem(String item, boolean empty) { boolean canEdit = authService != null && (authService.isAdmin() || authService.isModerator()); editButton.setVisible(canEdit); editButton.setManaged(canEdit); + deleteButton.setVisible(canEdit); + deleteButton.setManaged(canEdit); autoSelectFirstInMockMode(); } @@ -197,4 +202,29 @@ private void onEdit(ActionEvent event) { navigator.goTo( "/fxml/ResourceForm.fxml", c -> ((ResourceFormController) c).setResourceId(resourceId)); } + + @FXML + void onDelete(ActionEvent event) { + if (resourceId == null) return; + if (!confirmDelete()) return; + try { + services.getResourceService().delete(resourceId); + } catch (ServiceException e) { + showError(e.getMessage()); + return; + } + navigator.goTo("/fxml/ResourceList.fxml"); + } + + /** + * Диалог подтверждения удаления. Выделен в метод ради тестов: headless {@code Alert} блокирует + * поток, поэтому тесты переопределяют его без UI. + */ + protected boolean confirmDelete() { + var alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Удаление ресурса"); + alert.setHeaderText("Удалить ресурс?"); + alert.setContentText("Ресурс будет удалён без возможности восстановления."); + return alert.showAndWait().map(button -> button == ButtonType.OK).orElse(false); + } } diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java index 4a1d971..e187096 100644 --- a/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/ResourceListController.java @@ -13,7 +13,9 @@ import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.control.Alert; import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.TableColumn; @@ -33,6 +35,7 @@ public class ResourceListController extends Controller { @FXML private Label pageInfoLabel; @FXML private Label statusLabel; @FXML private TextField searchField; + @FXML private TextField descriptionField; @FXML private ComboBox sortCombo; @FXML private TableView resourceTable; @FXML private TableColumn titleColumn; @@ -40,12 +43,14 @@ public class ResourceListController extends Controller { @FXML private TableColumn createdColumn; @FXML private TableColumn updatedColumn; @FXML private Button createButton; + @FXML private Button deleteButton; @FXML private Button prevButton; @FXML private Button nextButton; @FXML private Button logoutButton; private final ObservableList rows = FXCollections.observableArrayList(); private long offset = 0; + private boolean canModify = false; // ===== Lifecycle ===== @@ -58,6 +63,7 @@ private void initialize() { setupRbac(); searchField.setOnAction(e -> reloadFromStart()); + descriptionField.setOnAction(e -> reloadFromStart()); sortCombo.setOnAction(e -> reloadFromStart()); resourceTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); @@ -94,6 +100,10 @@ private void setupTable() { resourceTable.setItems(rows); resourceTable.setPlaceholder(new Label("Документов пока нет")); + resourceTable + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldSel, newSel) -> updateDeleteButtonState()); resourceTable.setRowFactory( tv -> { @@ -110,9 +120,18 @@ private void setupTable() { private void setupRbac() { var authService = services == null ? null : services.getAuthService(); - boolean canCreate = authService != null && (authService.isAdmin() || authService.isModerator()); - createButton.setVisible(canCreate); - createButton.setManaged(canCreate); + canModify = authService != null && (authService.isAdmin() || authService.isModerator()); + createButton.setVisible(canModify); + createButton.setManaged(canModify); + deleteButton.setVisible(canModify); + deleteButton.setManaged(canModify); + updateDeleteButtonState(); + } + + private void updateDeleteButtonState() { + boolean hasSelection = + resourceTable != null && resourceTable.getSelectionModel().getSelectedItem() != null; + deleteButton.setDisable(!(canModify && hasSelection)); } // ===== Data loading ===== @@ -126,12 +145,7 @@ private void loadAsync() { hideError(); statusLabel.setText("Загрузка…"); - var filter = - new ResourceFilter( - searchField.getText() == null || searchField.getText().isBlank() - ? null - : searchField.getText().trim(), - null); + var filter = buildFilter(); var sort = resolveSort(); long currentOffset = offset; @@ -171,6 +185,17 @@ protected List call() { private record SortSpec(String sortBy, String sortDir) {} + private ResourceFilter buildFilter() { + return new ResourceFilter(normalizeQuery(searchField), normalizeQuery(descriptionField)); + } + + private static String normalizeQuery(TextField field) { + if (field == null || field.getText() == null || field.getText().isBlank()) { + return null; + } + return field.getText().trim(); + } + private SortSpec resolveSort() { String choice = sortCombo.getValue(); if (choice == null) return new SortSpec("createdAt", "desc"); @@ -197,6 +222,60 @@ private void onCreate(ActionEvent e) { navigator.goTo("/fxml/ResourceForm.fxml"); } + @FXML + void onDelete(ActionEvent e) { + var selected = resourceTable.getSelectionModel().getSelectedItem(); + if (selected == null) { + return; + } + if (!confirmDelete(selected)) { + return; + } + deleteAsync(selected); + } + + private void deleteAsync(Resource resource) { + hideError(); + statusLabel.setText("Удаление…"); + + var task = + new Task() { + @Override + protected Void call() { + services.getResourceService().delete(resource.getUuid()); + return null; + } + }; + + task.setOnSucceeded(e -> loadAsync()); + + task.setOnFailed( + e -> { + var ex = task.getException(); + String msg = + ex instanceof ServiceException se ? se.getMessage() : "Не удалось удалить ресурс"; + showError(msg); + statusLabel.setText(""); + }); + + var thread = new Thread(task, "resource-list-delete"); + thread.setDaemon(true); + thread.start(); + } + + /** + * Диалог подтверждения удаления. Выделен в метод ради тестов: headless {@code Alert} блокирует + * поток, поэтому тесты переопределяют его без UI. + */ + protected boolean confirmDelete(Resource resource) { + var alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Удаление ресурса"); + alert.setHeaderText("Удалить ресурс?"); + alert.setContentText( + "«" + resource.getTitle() + "» будет удалён без возможности восстановления."); + return alert.showAndWait().map(button -> button == ButtonType.OK).orElse(false); + } + @FXML private void onLogout(ActionEvent e) { services.getAuthService().logout(); diff --git a/src/main/resources/fxml/ResourceDetails.fxml b/src/main/resources/fxml/ResourceDetails.fxml index 7143b84..846e6a0 100644 --- a/src/main/resources/fxml/ResourceDetails.fxml +++ b/src/main/resources/fxml/ResourceDetails.fxml @@ -28,6 +28,10 @@ text="Редактировать" onAction="#onEdit" styleClass="btn-ghost"/> +