diff --git a/src/main/frontend/src/components/AddCatalogPriceForm.js b/src/main/frontend/src/components/AddCatalogPriceForm.js deleted file mode 100644 index e3a8234d3e..0000000000 --- a/src/main/frontend/src/components/AddCatalogPriceForm.js +++ /dev/null @@ -1,190 +0,0 @@ -// -// IMPORTANT: -// You must update ResourceUrl.RESOURCES_VERSION each time whenever you're modified this file! -// -// @todo #1342 AddCatalogPriceForm: add tests -// @todo #1388 AddCatalogPriceForm: consider using a tooltip for currency - -class AddCatalogPriceForm extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - price: null, - catalog: 'michel', - hasServerError: false, - validationErrors: [], - isDisabled: false - }; - this.handleSubmit = this.handleSubmit.bind(this); - this.handleChangePrice = this.handleChangePrice.bind(this); - this.handleChangeCatalog = this.handleChangeCatalog.bind(this); - } - - handleChangePrice(event) { - event.preventDefault(); - this.setState({ - price: event.target.value - }); - } - - handleChangeCatalog(event) { - event.preventDefault(); - this.setState({ - catalog: event.target.value - }); - } - - handleSubmit(event) { - event.preventDefault(); - - this.setState({ - isDisabled: true, - hasServerError: false, - validationErrors: [] - }); - - axios.patch( - this.props.url, - [ - { - op: 'add', - path: `/${this.state.catalog}_price`, - value: this.state.price - } - ], - { - headers: { - [this.props.csrfHeaderName]: this.props.csrfTokenValue, - 'Cache-Control': 'no-store' - }, - validateStatus: status => { - return status == 204 || status == 400; - } - } - ) - .then(response => { - const data = response.data; - if (data.hasOwnProperty('fieldErrors')) { - const fieldErrors = []; - if (data.fieldErrors.value) { - fieldErrors.push(...data.fieldErrors.value); - } - - this.setState({ - isDisabled: false, - validationErrors: fieldErrors - }); - return; - } - - // no need to reset the state as page will be reloaded - window.location.reload(); - }) - .catch(error => { - console.error(error); - this.setState({ isDisabled: false, hasServerError: true }); - }); - } - render() { - - return ( - - ); - } -} - -class AddCatalogPriceFormView extends React.PureComponent { - - getCurrencyByCatalogName(catalog) { - switch (catalog) { - case 'michel': - case 'yvert': - return ['\u20AC', 'EUR']; - case 'scott': - return ['$', 'USD']; - case 'gibbons': - return ['\u00A3', 'GBP']; - case 'solovyov': - case 'zagorski': - return ['\u20BD', 'RUB']; - } - } - render() { - const {handleSubmit, hasServerError, handleChangeCatalog, handleChangePrice, validationErrors, isDisabled, catalog, l10n} = this.props; - const hasValidationErrors = validationErrors.length > 0; - const [currencySymbol, currencyName] = this.getCurrencyByCatalogName(catalog); - return ( -
-
-
- { l10n['t_server_error'] || 'Server error' } -
-
- -
- -
-
-
- -
-
- { currencySymbol } - -
-
-
-
- - { validationErrors.join(', ') } - - -
-
-
- ); - } -} - -window.AddCatalogPriceForm = AddCatalogPriceForm; diff --git a/src/main/frontend/webpack.config.js b/src/main/frontend/webpack.config.js index d6ca663877..33c3da36da 100644 --- a/src/main/frontend/webpack.config.js +++ b/src/main/frontend/webpack.config.js @@ -8,7 +8,6 @@ module.exports = { 'utils/CatalogUtils': './src/utils/CatalogUtils.js', 'utils/DateUtils': './src/utils/DateUtils.js', - 'components/AddCatalogPriceForm': './src/components/AddCatalogPriceForm.js', 'components/AddReleaseYearForm': './src/components/AddReleaseYearForm.js', 'components/HideImageForm': './src/components/HideImageForm.js', 'components/SeriesSaleImportForm': './src/components/SeriesSaleImportForm.js', diff --git a/src/main/java/ru/mystamps/web/feature/series/AddCatalogPriceForm.java b/src/main/java/ru/mystamps/web/feature/series/AddCatalogPriceForm.java new file mode 100644 index 0000000000..90e434c193 --- /dev/null +++ b/src/main/java/ru/mystamps/web/feature/series/AddCatalogPriceForm.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009-2025 Slava Semushin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package ru.mystamps.web.feature.series; + +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +@Getter +@Setter +public class AddCatalogPriceForm { + @NotNull + private StampsCatalog catalogName; + + // @todo #1340 Update series: add validation for a price + @NotNull + private BigDecimal price; +} diff --git a/src/main/java/ru/mystamps/web/feature/series/HtmxSeriesController.java b/src/main/java/ru/mystamps/web/feature/series/HtmxSeriesController.java index 1b6e101069..ef5049d64b 100644 --- a/src/main/java/ru/mystamps/web/feature/series/HtmxSeriesController.java +++ b/src/main/java/ru/mystamps/web/feature/series/HtmxSeriesController.java @@ -34,6 +34,7 @@ import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.IOException; +import java.math.BigDecimal; @Controller @RequiredArgsConstructor @@ -132,5 +133,49 @@ public String addCatalogNumbers( return null; } + + @PatchMapping( + path = SeriesUrl.INFO_SERIES_PAGE, + headers = "HX-Trigger=add-catalog-price-form" + ) + public String addCatalogPrice( + @PathVariable("id") Integer seriesId, + @Valid AddCatalogPriceForm form, + BindingResult result, + @AuthenticationPrincipal CustomUserDetails currentUser, + Model model, + HttpServletResponse response + ) throws IOException { + + if (seriesId == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + if (!seriesService.isSeriesExist(seriesId)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + if (result.hasErrors()) { + response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY.value()); + model.addAttribute("isHtmx", true); + model.addAttribute("seriesId", seriesId); + return "series/info :: AddCatalogPriceForm"; + } + + Integer currentUserId = currentUser.getUserId(); + seriesService.addCatalogPrice( + form.getCatalogName(), + seriesId, + form.getPrice(), + currentUserId + ); + + // @todo #1671 AddCatalogPriceForm: update a page without full reload + response.addHeader("HX-Refresh", "true"); + + return null; + } } diff --git a/src/main/java/ru/mystamps/web/feature/series/RestSeriesController.java b/src/main/java/ru/mystamps/web/feature/series/RestSeriesController.java index 856eb68f87..623cfc2982 100644 --- a/src/main/java/ru/mystamps/web/feature/series/RestSeriesController.java +++ b/src/main/java/ru/mystamps/web/feature/series/RestSeriesController.java @@ -18,7 +18,6 @@ package ru.mystamps.web.feature.series; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; @@ -36,7 +35,6 @@ import javax.validation.constraints.NotEmpty; import java.io.IOException; import java.util.List; -import java.util.Locale; @Validated @RestController @@ -46,7 +44,6 @@ class RestSeriesController { private final SeriesService seriesService; private final SeriesImageService seriesImageService; - // @todo #1340 Update series: add validation for a price // @todo #1343 Update series: add validation for a release year @PatchMapping(SeriesUrl.INFO_SERIES_PAGE) public ResponseEntity updateSeries( @@ -77,19 +74,6 @@ public ResponseEntity updateSeries( case "/release_year": seriesService.addReleaseYear(seriesId, patch.integerValue(), currentUserId); break; - case "/michel_price": - case "/scott_price": - case "/yvert_price": - case "/gibbons_price": - case "/solovyov_price": - case "/zagorski_price": - seriesService.addCatalogPrice( - extractCatalog(path), - seriesId, - patch.bigDecimalValue(), - currentUserId - ); - break; default: // @todo #785 Update series: properly fail on invalid path break; @@ -116,12 +100,5 @@ public ResponseEntity modifySeriesImage( return ResponseEntity.noContent().build(); } - private static StampsCatalog extractCatalog(String path) { - // "/catalog_something" => "catalog" => "CATALOG" - String catalogName = StringUtils.substringBetween(path, "/", "_") - .toUpperCase(Locale.ENGLISH); - return StampsCatalog.valueOf(catalogName); - } - } diff --git a/src/main/java/ru/mystamps/web/feature/site/ResourceUrl.java b/src/main/java/ru/mystamps/web/feature/site/ResourceUrl.java index 12f01abf0e..334627b2f4 100644 --- a/src/main/java/ru/mystamps/web/feature/site/ResourceUrl.java +++ b/src/main/java/ru/mystamps/web/feature/site/ResourceUrl.java @@ -31,7 +31,7 @@ public final class ResourceUrl { public static final String STATIC_RESOURCES_URL = "https://stamps.filezz.ru"; // MUST be updated when any of our resources were modified - public static final String RESOURCES_VERSION = "v0.4.7.0"; + public static final String RESOURCES_VERSION = "v0.4.7.2"; private static final String CATALOG_UTILS_JS = "/public/js/" + RESOURCES_VERSION + "/utils/CatalogUtils.min.js"; private static final String COLLECTION_INFO_JS = "/public/js/" + RESOURCES_VERSION + "/collection/info.min.js"; @@ -42,7 +42,6 @@ public final class ResourceUrl { private static final String SERIES_INFO_JS = "/public/js/" + RESOURCES_VERSION + "/series/info.min.js"; private static final String SALE_IMPORT_FORM_JS = "/public/js/" + RESOURCES_VERSION + "/components/SeriesSaleImportForm.min.js"; private static final String SIMILAR_SERIES_FORM_JS = "/public/js/" + RESOURCES_VERSION + "/components/SimilarSeriesForm.min.js"; - private static final String CATALOG_PRICE_FORM_JS = "/public/js/" + RESOURCES_VERSION + "/components/AddCatalogPriceForm.min.js"; private static final String RELEASE_YEAR_FORM_JS = "/public/js/" + RESOURCES_VERSION + "/components/AddReleaseYearForm.min.js"; private static final String HIDE_IMAGE_FORM_JS = "/public/js/" + RESOURCES_VERSION + "/components/HideImageForm.min.js"; private static final String SERIES_SALES_LIST_JS = "/public/js/" + RESOURCES_VERSION + "/components/SeriesSalesList.min.js"; @@ -81,7 +80,6 @@ public static void exposeResourcesToView(Map resources, String h put(resources, host, "SERIES_INFO_JS", SERIES_INFO_JS); put(resources, host, "SALE_IMPORT_FORM_JS", SALE_IMPORT_FORM_JS); put(resources, host, "SIMILAR_SERIES_FORM_JS", SIMILAR_SERIES_FORM_JS); - put(resources, host, "CATALOG_PRICE_FORM_JS", CATALOG_PRICE_FORM_JS); put(resources, host, "RELEASE_YEAR_FORM_JS", RELEASE_YEAR_FORM_JS); put(resources, host, "HIDE_IMAGE_FORM_JS", HIDE_IMAGE_FORM_JS); put(resources, host, "SERIES_SALES_LIST_JS", SERIES_SALES_LIST_JS); diff --git a/src/main/java/ru/mystamps/web/support/spring/mvc/PatchRequest.java b/src/main/java/ru/mystamps/web/support/spring/mvc/PatchRequest.java index e5c82fa702..4032060b46 100644 --- a/src/main/java/ru/mystamps/web/support/spring/mvc/PatchRequest.java +++ b/src/main/java/ru/mystamps/web/support/spring/mvc/PatchRequest.java @@ -24,7 +24,6 @@ import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; -import java.math.BigDecimal; // See for details: http://jsonpatch.com @Getter @@ -58,8 +57,4 @@ public Integer integerValue() { return Integer.valueOf(value); } - public BigDecimal bigDecimalValue() { - return BigDecimalConverter.valueOf(value); - } - } diff --git a/src/main/javascript/series/info.js b/src/main/javascript/series/info.js index 1acb0ece42..df4c4a5e83 100644 --- a/src/main/javascript/series/info.js +++ b/src/main/javascript/series/info.js @@ -7,3 +7,40 @@ function populateTransactionDateWithTodayDate() { var today = DateUtils.formatDateToDdMmYyyy(new Date()); $('#date').val(today); } + +function getCurrencyByCatalogName(catalog) { + switch (catalog) { + case 'MICHEL': + case 'YVERT': + return [ '\u20AC', 'EUR' ]; + case 'SCOTT': + return [ '$', 'USD' ]; + case 'GIBBONS': + return [ '\u00A3', 'GBP' ]; + case 'SOLOVYOV': + case 'ZAGORSKI': + return [ '\u20BD', 'RUB' ]; + } +} + +function initPriceCatalog() { + var catalogNameElem = document.getElementById('price-catalog-name'); + if (catalogNameElem == null) { + console.error("Couldn't initialize catalog name selector: element not found"); + return; + } + catalogNameElem.addEventListener('change', function changeCatalogCurrency(elem) { + var name = this.value; + var info = getCurrencyByCatalogName(this.value); + var symbolElem = document.getElementById('js-catalog-price-symbol'); + if (symbolElem == null) { + console.error("Couldn't change currency symbol: element not found"); + } + var titleElem = document.getElementById('catalog-price'); + if (titleElem == null) { + console.error("Couldn't change currency title: element not found"); + } + symbolElem.innerText = info[0]; + titleElem.title = info[1]; + }); +} diff --git a/src/main/webapp/WEB-INF/views/series/info.html b/src/main/webapp/WEB-INF/views/series/info.html index 0afd487130..9dbc3e9969 100644 --- a/src/main/webapp/WEB-INF/views/series/info.html +++ b/src/main/webapp/WEB-INF/views/series/info.html @@ -409,7 +409,100 @@
Hidden images
-
+
+
+
+ +
+ +
+ + +
+
+
+ +
+ +
+ + +
+
+ +
+
+ +
+
+
+
@@ -1027,7 +1120,7 @@
Add info about selling/buying thi src="../../../../../../target/classes/js/utils/DateUtils.min.js" th:src="${DATE_UTILS_JS}"> - @@ -1074,6 +1167,16 @@
Add info about selling/buying thi } } }); + + // https://htmx.org/events/#htmx:load + document.body.addEventListener('htmx:load', function htmxLoadEventHandler(event) { + // NOTE: htmx:load event fires in the beginning, when a page has been loaded, and when a new element + // has been placed after AJAX request. In both cases we need to register custom event listener for the + // add catalog price form. Also, we don't need to do anything, if updated element belongs to another form. + if (event.detail.elt.querySelector('#price-catalog-name') != null) { + initPriceCatalog(); + } + }); /*/--> -