diff --git a/sql/migration.sql b/sql/migration.sql index 42dc9de62e0ad5817b98f86f5d91ac29e5f2c6e8..28523590aa79a3a2e37ccc761f0ede5d77513ffe 100644 --- a/sql/migration.sql +++ b/sql/migration.sql @@ -208,6 +208,25 @@ BEGIN ALTER TABLE indicator DROP CONSTRAINT "FK_indicator_i18n_description"; ALTER TABLE indicator ADD CONSTRAINT "FK_indicator_i18n_description" FOREIGN KEY (description) REFERENCES i18nkey (id); CREATE INDEX "IX_dailyvalue_doy" ON dailyvalue (EXTRACT(DOY FROM date), indicator); + UPDATE i18n SET translation=REPLACE(translation, ' (tmax', ' (Tmax') + RETURN true; +END +$BODY$ +language plpgsql; + +CREATE OR REPLACE FUNCTION upgrade20240426() RETURNS boolean AS $BODY$ +BEGIN + CREATE INDEX IF NOT EXISTS "IX_cellpra" ON cellpra (cell,pra); + UPDATE i18n SET translation=REPLACE(translation, 'tmax', 'Tmax'); + RETURN true; +END +$BODY$ +language plpgsql; + +CREATE OR REPLACE FUNCTION upgrade20240426() RETURNS boolean AS $BODY$ +BEGIN + CREATE INDEX IF NOT EXISTS "IX_cellpra" ON cellpra (cell,pra); + UPDATE i18n SET translation=REPLACE(translation, 'tmax', 'Tmax'); RETURN true; END $BODY$ diff --git a/sql/schema.tables.sql b/sql/schema.tables.sql index 85a7f2d37b9e00b9f6f62837cfb7a979fe4356a2..03ddaca73fdd2e4363d8041eb168aa9daefdfdbb 100644 --- a/sql/schema.tables.sql +++ b/sql/schema.tables.sql @@ -122,6 +122,7 @@ CREATE TABLE IF NOT EXISTS cellpra ( CONSTRAINT "FK_cellpra_cell" FOREIGN KEY (cell) REFERENCES cell (id), CONSTRAINT "FK_cellpra_pra" FOREIGN KEY (pra) REFERENCES pra (id) ); +CREATE INDEX IF NOT EXISTS "IX_cellpra" ON cellpra (cell,pra); COMMENT ON TABLE cellpra IS 'Proportion of one cell in a PRA.'; COMMENT ON COLUMN cellpra.cell IS 'Cell in a PRA.'; COMMENT ON COLUMN cellpra.pra IS 'PRA containing at least a part of the cell.'; diff --git a/sql/translations.csv b/sql/translations.csv index df51c15625c4392de7900de35a36e6579173606f..0181ecf27c8559fe6967baf53825d5b29dccab87 100644 --- a/sql/translations.csv +++ b/sql/translations.csv @@ -5,12 +5,12 @@ frostdaystmin-description,en,Number of frost days (Tmin < 0 °C) frostdaystmin-description,fr,Nombre de jours de gel (Tmin < 0 °C) hdaystmax1,en,Hot days hdaystmax1,fr,Jours chauds -hdaystmax1-description,en,Number of hot days (tmax > 25 °C) -hdaystmax1-description,fr,Nombre de jours chauds (tmax > 25 °C) +hdaystmax1-description,en,Number of hot days (Tmax > 25 °C) +hdaystmax1-description,fr,Nombre de jours chauds (Tmax > 25 °C) hdaystmax,en,Hot days hdaystmax,fr,Jours chauds -hdaystmax-description,en,Number of hot days (tmax > 35 °C) -hdaystmax-description,fr,Nombre de jours chauds (tmax > 35 °C) +hdaystmax-description,en,Number of hot days (Tmax > 35 °C) +hdaystmax-description,fr,Nombre de jours chauds (Tmax > 35 °C) maxt,en,Average of maximal temperatures maxt,fr,Moyenne des températures maximales maxt-description,en,Average of maximal temperatures diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/ui/CollapsedSelect.java b/www-client/src/main/java/fr/agrometinfo/www/client/ui/CollapsedSelect.java new file mode 100644 index 0000000000000000000000000000000000000000..e0c1396357b83bd32f38c4cda90ddf59ef6f2073 --- /dev/null +++ b/www-client/src/main/java/fr/agrometinfo/www/client/ui/CollapsedSelect.java @@ -0,0 +1,100 @@ +package fr.agrometinfo.www.client.ui; + +import org.jboss.elemento.Elements; +import org.jboss.elemento.EventCallbackFn; +import org.jboss.elemento.EventType; +import org.jboss.elemento.HtmlContentBuilder; + +import elemental2.dom.HTMLDivElement; +import elemental2.dom.HTMLElement; +import elemental2.dom.MouseEvent; + +/** + * A component which mimics the Select component when the drop down list is + * closed. + * + * @author Olivier Maury + */ +public final class CollapsedSelect { + /** + * The root element. + */ + private final HtmlContentBuilder<HTMLDivElement> root; + /** + * Span displaying the prompt. + */ + private final HtmlContentBuilder<HTMLElement> promptSpan = Elements.span(); + /** + * Span displaying the displayed text of the selected item. + */ + private final HtmlContentBuilder<HTMLElement> selectionSpan = Elements.span(); + + /** + * Constructor. + * + * @param promptText prompt text in the field label + * @param selectionText displayed text of the selected item + */ + public CollapsedSelect(final String promptText, final String selectionText) { + this.root = Elements.div().css("field-group lined d-select floating")// + .add(Elements.div().css("field-cntr") // + .add(Elements.div().css("flex-layout") // + .add(Elements.div().css("flex-item field-input-cntr") // + .add(Elements.label().css("field-label") // + .add(promptSpan)) + .add(Elements.button().css("select-button") + .add(Elements.span().css("select-value ellipsis-text") // + .add(selectionSpan)))) // + .add(Elements.div().css("field-mandatory-addon") // + .add(Elements.i() // + .css("mdi mdi-menu-down mdi-24px clickable-icon waves-effect"))))) + .hidden(false); + setPromptText(promptText); + setSelectionText(selectionText); + } + + /** + * @return root element + */ + public HtmlContentBuilder<HTMLDivElement> getRoot() { + return root; + } + + /** + * Hide the widget. + */ + public void hide() { + root.hidden(true); + } + + /** + * Add the given callback to the root element. + * + * @param callback + */ + public void onClick(final EventCallbackFn<MouseEvent> callback) { + root.on(EventType.click, callback); + } + + /** + * @param promptText prompt text in the field label + */ + public void setPromptText(final String promptText) { + promptSpan.textContent(promptText); + } + + /** + * @param selectionText displayed text of the selected item + */ + public void setSelectionText(final String selectionText) { + selectionSpan.textContent(selectionText); + } + + /** + * Show the widget. + */ + public void show() { + root.hidden(false); + } + +} diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/ui/DominoListBuilder.java b/www-client/src/main/java/fr/agrometinfo/www/client/ui/DominoListBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..93e6a90f57c27bce82424137b24035c599cb455b --- /dev/null +++ b/www-client/src/main/java/fr/agrometinfo/www/client/ui/DominoListBuilder.java @@ -0,0 +1,125 @@ +package fr.agrometinfo.www.client.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.function.Consumer; + +import org.dominokit.domino.ui.grid.flex.FlexItem; +import org.dominokit.domino.ui.grid.flex.FlexLayout; +import org.dominokit.domino.ui.lists.ListGroup; +import org.jboss.elemento.Elements; + +import elemental2.dom.HTMLDivElement; + +/** + * Builder for a Domino {@link ListGroup}, a component that allow selecting a + * single option from a panel. + * + * @author Olivier Maury + * + * @param <T> The type of a single option value + */ +public final class DominoListBuilder<T> extends SelectBuilder<T, ListGroup<T>> { + /** + * The component. + */ + private ListGroup<T> select; + + @Override + public DominoListBuilder<T> addOption(final T option) { + return addOptions(Arrays.asList(option)); + } + + @Override + public DominoListBuilder<T> addOptions(final Collection<T> list) { + getSelect().addItems(new ArrayList<>(list)); + return this; + } + + /** + * Add handler on the displayed text when value change. + * + * @param handler the handler receives the displayed text of selected option + * @return same builder instance + */ + public DominoListBuilder<T> addTextChangeHandler(final Consumer<String> handler) { + getSelect().addSelectionListener(selectedItems -> { + if (selectedItems == null || selectedItems.isEmpty()) { + return; + } + final T item = selectedItems.get(0).getValue(); + handler.accept(getTextFunction().apply(item)); + }); + return this; + } + + @Override + public DominoListBuilder<T> addValueChangeHandler(final Consumer<String> handler) { + getSelect().addSelectionListener(selectedItems -> { + if (selectedItems == null || selectedItems.isEmpty()) { + return; + } + final T item = selectedItems.get(0).getValue(); + handler.accept(getValueFunction().apply(item)); + }); + return this; + } + + @Override + public ListGroup<T> build() { + Objects.requireNonNull(getValueFunction(), "setValueFunction() must be called before!"); + Objects.requireNonNull(getTextFunction(), "setTextFunction() must be called before!"); + return getSelect().setItemRenderer( + (listGroup, item) -> { + final FlexItem<HTMLDivElement> flexItem = FlexItem.create() // + .setFlexGrow(1) // + .appendChild(// + Elements.span().textContent(getTextFunction().apply(item.getValue()))); + item.setSelectable(true) + .appendChild( + FlexLayout.create() // + .appendChild(flexItem)); + }); + } + + private ListGroup<T> getSelect() { + if (select == null) { + select = ListGroup.<T>create(); + } + return select; + } + + @Override + public DominoListBuilder<T> removeOptions() { + getSelect().clearElement(); + return this; + } + + @Override + public DominoListBuilder<T> select(final T value) { + getSelect().getItems().stream() // + .filter(i -> Objects.equals(getValueFunction().apply(i.getValue()), getValueFunction().apply(value))) // + .findFirst() // + .ifPresent(toSelect -> select.select(Arrays.asList(toSelect))); + return this; + } + + @Override + public DominoListBuilder<T> setPrompt(final String text) { + // no prompt for this component + return this; + } + + /** + * Use an existing element. + * + * @param value existing ListGroup element + * @return same builder instance + */ + public DominoListBuilder<T> setSelect(final ListGroup<T> value) { + select = value; + return this; + } +} diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java index bc76609376e6bd025246276cbe39325ebc128243..9708094f031033171c368d846f2ae331e6d48567 100644 --- a/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java +++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java @@ -18,6 +18,7 @@ import org.dominokit.domino.ui.grid.flex.FlexItem; import org.dominokit.domino.ui.icons.Icons; import org.dominokit.domino.ui.icons.MdiIcon; import org.dominokit.domino.ui.layout.Layout; +import org.dominokit.domino.ui.lists.ListGroup; import org.dominokit.domino.ui.loaders.Loader; import org.dominokit.domino.ui.loaders.LoaderEffect; import org.dominokit.domino.ui.menu.Menu; @@ -40,6 +41,7 @@ import elemental2.dom.DomGlobal; import elemental2.dom.HTMLAnchorElement; import elemental2.dom.HTMLDivElement; import elemental2.dom.HTMLElement; +import elemental2.dom.HTMLLabelElement; import elemental2.dom.MouseEvent; import fr.agrometinfo.www.client.App; import fr.agrometinfo.www.client.event.LoadingEvent; @@ -50,6 +52,8 @@ import fr.agrometinfo.www.client.presenter.LayoutPresenter; import fr.agrometinfo.www.client.presenter.MapPresenter; import fr.agrometinfo.www.client.presenter.RightPanelPresenter; import fr.agrometinfo.www.client.ui.AgroclimAppsMenu; +import fr.agrometinfo.www.client.ui.CollapsedSelect; +import fr.agrometinfo.www.client.ui.DominoListBuilder; import fr.agrometinfo.www.client.ui.DominoSelectBuilder; import fr.agrometinfo.www.client.util.ApplicationUtils; import fr.agrometinfo.www.shared.dto.ChoiceDTO; @@ -150,7 +154,7 @@ implements LayoutPresenter.View, LoadingHandler { /** * Selector for indicators. */ - private final Select<IndicatorDTO> indicatorSelect = Select.create(CSTS.chooseIndicator()); + private final ListGroup<IndicatorDTO> indicatorSelect = ListGroup.create(); /** * Application layout. @@ -218,6 +222,11 @@ implements LayoutPresenter.View, LoadingHandler { */ private final HtmlContentBuilder<HTMLDivElement> links = div().css("links"); + /** + * The replacement of indicatorSelect when it is hidden. + */ + private final CollapsedSelect collapsedIndicatorSelect = new CollapsedSelect(CSTS.chooseIndicator(), ""); + /** * @param text link text * @param callback {@link EventCallbackFn<MouseEvent>} to be added to the click @@ -339,15 +348,37 @@ implements LayoutPresenter.View, LoadingHandler { .setSelect(periodSelect) // .addValueChangeHandler(this::onPeriodChange) // .build(); + periodSelect.setSearchable(false); // GWT.log("LayoutView.initLeftPanel() indicators"); - panel.appendChild(indicatorSelect); - new DominoSelectBuilder<IndicatorDTO>() // + collapsedIndicatorSelect.hide(); + panel.appendChild(collapsedIndicatorSelect.getRoot()); + final HtmlContentBuilder<HTMLDivElement> indicatorPanel = div().css("field-group lined list-group"); + final HtmlContentBuilder<HTMLLabelElement> indicatorLabel = Elements.label() // + .css("field-label") // + .add(Elements.span().textContent(CSTS.chooseIndicator())); + panel.appendChild(indicatorPanel// + .add(indicatorLabel) // + .add(indicatorSelect) // + ); + new DominoListBuilder<IndicatorDTO>() // .setSelect(indicatorSelect) // + .addTextChangeHandler(collapsedIndicatorSelect::setSelectionText) // + .setTextFunction(IndicatorDTO::getDescription) // + .setValueFunction(IndicatorDTO::getCode) // .addValueChangeHandler(this::onIndicatorChange) // .build(); + indicatorLabel.on(EventType.click, e -> { + indicatorPanel.hidden(true); + collapsedIndicatorSelect.show(); + }); + collapsedIndicatorSelect.onClick(e -> { + indicatorPanel.hidden(false); + collapsedIndicatorSelect.hide(); + }); + // GWT.log("LayoutView.initLeftPanel() regions"); panel.appendChild(regionSelect); @@ -355,6 +386,7 @@ implements LayoutPresenter.View, LoadingHandler { .setSelect(regionSelect) // .addValueChangeHandler(this::onRegionChange) // .build(); + regionSelect.setSearchable(false); // GWT.log("LayoutView.initLeftPanel() year"); @@ -362,6 +394,7 @@ implements LayoutPresenter.View, LoadingHandler { new DominoSelectBuilder<Entry<String, String>>() // .setSelect(yearSelect) // .addValueChangeHandler(this::onYearChange); + yearSelect.setSearchable(false); // GWT.log("LayoutView.initLeftPanel() comparison"); @@ -546,7 +579,7 @@ implements LayoutPresenter.View, LoadingHandler { } } if (list == null || list.isEmpty()) { - indicatorSelect.removeAllOptions(); + indicatorSelect.clearElement(); return; } GWT.log("LayoutView.onPeriodChange() Indicators : " + list); @@ -554,7 +587,7 @@ implements LayoutPresenter.View, LoadingHandler { final IndicatorDTO defaultIndicator = list.stream() // .filter(i -> DEFAULT_INDICATOR.equals(i.getCode())) // .findFirst().orElse(firstIndicator); - new DominoSelectBuilder<IndicatorDTO>() // + new DominoListBuilder<IndicatorDTO>() // .setSelect(indicatorSelect) // .setTextFunction(IndicatorDTO::getDescription) // .setValueFunction(IndicatorDTO::getCode) // diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css b/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css index cada1db6d0e23045418837858f689f73d5d6a459..bdbff6757d2439f49e3878c52edc502ef39cc5f0 100644 --- a/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css +++ b/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css @@ -180,6 +180,39 @@ details.card-details[open] > summary i { transform: rotate(180deg); } +div.list-group > label.field-label { + margin-bottom: 0; + padding-left: 6px; + padding-right: 6px; + position: relative; + top: 0px; + width: 100%; +} +div.list-group > label.field-label:after { + content: "\F360"; + text-align: right; + float: right; + font: normal normal normal 24px/1 "Material Design Icons"; +} +div.list-group .flex-item { + line-height: 2em; + padding-left: 6px; + padding-right: 6px; + transition: all .5s; + user-select: none; +} +div.list-group .d-list-item.selected { + background-color: #ededed; + color: #2a2a2a; +} +div.list-group .d-list-item.selected span:after { + content: "✓"; + text-align: right; + float:right; +} +div.list-group .d-list-item:hover { + background-color: #f5f5f5; +} .float-right { float: right; diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/HasId.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/HasId.java deleted file mode 100644 index f889b403a9f842929d6834c1b355bd80cc9c0b9d..0000000000000000000000000000000000000000 --- a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/HasId.java +++ /dev/null @@ -1,18 +0,0 @@ -package fr.agrometinfo.www.shared.dto; - -/** - * The object has an identifier. - * - * @author Olivier Maury - */ -public interface HasId { - /** - * @return identifier - */ - Integer getId(); - - /** - * @param id identifier - */ - void setId(Integer id); -} diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/HasName.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/HasName.java deleted file mode 100644 index 75c0d0726cb6191ea11c1ec1b97652a5f2ae570e..0000000000000000000000000000000000000000 --- a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/HasName.java +++ /dev/null @@ -1,18 +0,0 @@ -package fr.agrometinfo.www.shared.dto; - -/** - * The object as name. - * - * @author Olivier Maury - */ -public interface HasName { - /** - * @return name - */ - String getName(); - - /** - * @param name name - */ - void setName(String name); -}