diff --git a/app/assets/stylesheets/Admin.scss b/app/assets/stylesheets/Admin.scss index aef9b29a8d..823dac7709 100644 --- a/app/assets/stylesheets/Admin.scss +++ b/app/assets/stylesheets/Admin.scss @@ -9,7 +9,7 @@ $LOGO_BG_COLOR: yellow; $LOGO_HOVER_FG_COLOR: purple; $LOGO_HOVER_BG_COLOR: yellow; -$LEFT_BAR_BORDER_COLOR: #535454; // gray +$LEFT_BAR_BORDER_COLOR: #535354; // gray $LEFT_BAR_BORDER_RADIUS: 0px; $LEFT_BAR_HEADER_FG_COLOR: black; $LEFT_BAR_HEADER_BG_COLOR: yellow; diff --git a/app/assets/stylesheets/Agaricus.scss b/app/assets/stylesheets/Agaricus.scss index de32becc3c..8cc478c8e5 100644 --- a/app/assets/stylesheets/Agaricus.scss +++ b/app/assets/stylesheets/Agaricus.scss @@ -1,7 +1,7 @@ @import "defaults"; $augustus_cap: #ECCF95; -$brasiliensis_gills_1: #BF6362; // #A06463 +$brasiliensis_gills_1: #BF6262; // #A06463 $brasiliensis_gills_2: #743931; $campestris_cap: #F6F0F2; $cupreobrunneus_gills: #3B2821; diff --git a/app/assets/stylesheets/Amanita.scss b/app/assets/stylesheets/Amanita.scss index 8439d52e28..199d8a63e1 100644 --- a/app/assets/stylesheets/Amanita.scss +++ b/app/assets/stylesheets/Amanita.scss @@ -13,7 +13,7 @@ $calyptroderma_middle_cap: #c18346; $muscaria_background: #cc2616; $muscaria_foreground: #fff8c6; $velosa_background: #dd9d5f; -$velosa_light_veil: #f9e9d3; // faebd4 +$velosa_light_veil: #f9e8d3; // faebd4 $velosa_dark_veil: #f4d5a6; $novinupta_background: #d1afa5; $pachycolea_background: #383138; diff --git a/app/assets/stylesheets/BlackOnWhite.scss b/app/assets/stylesheets/BlackOnWhite.scss index beb7ee2336..2809a5de38 100644 --- a/app/assets/stylesheets/BlackOnWhite.scss +++ b/app/assets/stylesheets/BlackOnWhite.scss @@ -4,7 +4,7 @@ $LOGO_BORDER_COLOR: #DDDDDD; $LEFT_BAR_BORDER_COLOR: #DDDDDD; -$TOP_BAR_BORDER_COLOR: #D9D9Da; +$TOP_BAR_BORDER_COLOR: #D9D9D9; $LIST_BORDER_COLOR: #DDDDDD; $BUTTON_HOVER_BORDER_COLOR: #CCCCCC; $BUTTON_BG_COLOR: #CCCCCC; diff --git a/app/assets/stylesheets/Cantharellaceae.scss b/app/assets/stylesheets/Cantharellaceae.scss index 5fc8c120c1..22901b7443 100644 --- a/app/assets/stylesheets/Cantharellaceae.scss +++ b/app/assets/stylesheets/Cantharellaceae.scss @@ -11,7 +11,7 @@ $tubaeformis_hymenium: #c2914c; $tubaeformis_bright_stipe: #ffb230; $tubaeformis_dark_stipe: #4b2e0c; $tubaeformis_light_stipe: #e5bb67; -$cornucopioides_dark_hymenium: #12120d; // image 465 #10110b +$cornucopioides_dark_hymenium: #13120d; // image 465 #10110b $cornucopioides_light_hymenium: #9b9690; $cornucopioides_dark_cap: #4f4337; $cornucopioides_light_cap: #826c57; diff --git a/app/assets/stylesheets/Hygrocybe.scss b/app/assets/stylesheets/Hygrocybe.scss index 6546aca668..7cbf4e55b7 100644 --- a/app/assets/stylesheets/Hygrocybe.scss +++ b/app/assets/stylesheets/Hygrocybe.scss @@ -1,6 +1,6 @@ @import "defaults"; -$conica_stain: #34342d; // #37372f +$conica_stain: #34342c; // #37372f $conica_cap_red: #a31404; $conica_cap_orange: #dd6226; $conica_cap_yellow: #ffbf01; diff --git a/app/assets/stylesheets/Sudo.scss b/app/assets/stylesheets/Sudo.scss index 06ffc4e641..2687d767b8 100644 --- a/app/assets/stylesheets/Sudo.scss +++ b/app/assets/stylesheets/Sudo.scss @@ -1,6 +1,6 @@ @import "defaults"; -$BODY_BG_COLOR: #DE7200; // #DD7700 +$BODY_BG_COLOR: #DE7201; // #DD7700 $LOGO_BORDER_COLOR: black; $LOGO_BORDER_WIDTH: 2px; // vs 1px in default diff --git a/app/assets/stylesheets/mo/_autocomplete.scss b/app/assets/stylesheets/mo/_autocomplete.scss index e7e75d7ce3..af1909e111 100644 --- a/app/assets/stylesheets/mo/_autocomplete.scss +++ b/app/assets/stylesheets/mo/_autocomplete.scss @@ -12,6 +12,15 @@ } } +.keep-btn { + display: none; +} +.create { + .keep-btn { + display: inline-block; + } +} + // initially we may not have id, but we also don't offer create // until they've typed something .create-button { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9c12d701d6..e1f6b95400 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -788,7 +788,12 @@ def flash_object_errors(obj) def save_with_log(obj) type_sym = obj.class.to_s.underscore.to_sym if obj.save - flash_notice(:runtime_created_at.t(type: type_sym)) + notice = if obj.respond_to?(:text_name) && (name = obj.text_name) + :runtime_created_name.t(type: type_sym, value: name) + else + :runtime_created_at.t(type: type_sym) + end + flash_notice(notice) true else flash_error(:runtime_no_save.t(type: type_sym)) diff --git a/app/controllers/herbaria_controller.rb b/app/controllers/herbaria_controller.rb index 912470f780..b45fee660b 100644 --- a/app/controllers/herbaria_controller.rb +++ b/app/controllers/herbaria_controller.rb @@ -45,7 +45,10 @@ # See https://tinyurl.com/ynapvpt7 # View and modify Herbaria (displayed as "Fungaria") +# rubocop:disable Metrics/ClassLength class HerbariaController < ApplicationController + include ::Locationable + before_action :login_required # only: [:create, :destroy, :edit, :new, :update] before_action :store_location, only: [:create, :edit, :new, :show, :update] @@ -99,24 +102,69 @@ def show def new @herbarium = Herbarium.new + respond_to do |format| + format.turbo_stream { render_modal_herbarium_form } + format.html + end end def edit @herbarium = find_or_goto_index(Herbarium, params[:id]) - return unless @herbarium - return unless make_sure_can_edit! + return unless @herbarium && make_sure_can_edit! + + set_up_herbarium_for_edit + respond_to do |format| + format.turbo_stream { render_modal_herbarium_form } + format.html + end + end + def set_up_herbarium_for_edit @herbarium.place_name = @herbarium.location.try(&:name) @herbarium.personal = @herbarium.personal_user_id.present? @herbarium.personal_user_name = @herbarium.personal_user.try(&:login) end + def render_modal_herbarium_form + render(partial: "shared/modal_form", + locals: { title: modal_title, action: modal_form_action, + identifier: modal_identifier, local: false, + form: "herbaria/form" }) and return + end + + def modal_identifier + case action_name + when "new", "create" + "herbarium" + when "edit", "update" + "herbarium_#{@herbarium.id}" + end + end + + def modal_title + case action_name + when "new", "create" + :create_herbarium_title.l + when "edit", "update" + :edit_herbarium_title.l + end + end + + def modal_form_action + case action_name + when "new", "create" then :create + when "edit", "update" then :update + end + end + # ---------- Actions to Modify data: (create, update, destroy, etc.) --------- def create @herbarium = Herbarium.new(herbarium_params) normalize_parameters - return render(:new) unless validate_herbarium! + create_location_object_if_new(@herbarium) + try_to_save_location_if_new(@herbarium) + return render(:new) unless validate_herbarium! && !@any_errors @herbarium.save @herbarium.add_curator(@user) if @herbarium.personal_user @@ -130,7 +178,9 @@ def update @herbarium.attributes = herbarium_params normalize_parameters - return unless validate_herbarium! + create_location_object_if_new(@herbarium) + try_to_save_location_if_new(@herbarium) + return unless validate_herbarium! && !@any_errors @herbarium.save redirect_to_create_location_or_referrer_or_show_location @@ -265,6 +315,10 @@ def validate_admin_personal_user! flash_notice( :edit_herbarium_successfully_made_personal.t(user: user.login) ) + update_personal_herbarium(user) + end + + def update_personal_herbarium(user) @herbarium.curators.clear @herbarium.add_curator(user) @herbarium.personal_user_id = user.id @@ -326,7 +380,7 @@ def user_can_destroy_herbarium? def redirect_to_create_location_or_referrer_or_show_location redirect_to_create_location || redirect_to_referrer || - redirect_with_query(herbarium_path(@herbarium)) + show_modal_flash_or_show_herbarium end def redirect_to_create_location @@ -339,11 +393,39 @@ def redirect_to_create_location true end + # this updates both the form and the flash + def reload_herbarium_modal_form_and_flash + render( + partial: "shared/modal_form_reload", + locals: { identifier: modal_identifier, form: "herbaria/form" } + ) and return true + end + + # What to do if the save succeeds + def show_modal_flash_or_show_herbarium + respond_to do |format| + format.html do + redirect_with_query(herbarium_path(@herbarium)) and return + end + format.turbo_stream do + # Context here is the obs form. + flash_notice( + :runtime_created_name.t(type: :herbarium, value: @herbarium.name) + ) + flash_notice( + :runtime_added_to.t(type: :herbarium, name: :observation) + ) + render(partial: "herbaria/update_observation") and return + end + end + end + def herbarium_params return {} unless params[:herbarium] params.require(:herbarium). - permit(:name, :code, :email, :mailing_address, :description, + permit(:name, :code, :email, :mailing_address, :description, :location_id, :place_name, :personal, :personal_user_name) end end +# rubocop:enable Metrics/ClassLength diff --git a/app/extensions/string_extensions.rb b/app/extensions/string_extensions.rb index c16983359f..f78cce1a70 100644 --- a/app/extensions/string_extensions.rb +++ b/app/extensions/string_extensions.rb @@ -23,6 +23,8 @@ # gsub_html_special_chars:: auxiliary to html_to_ascii # unescape_html:: Render special encoded characters as regular characters # as_displayed:: Render everything humanly legible, for integration tests +# id_of_nested_field:: Rails generates `observation_notes` for the ID of a +# nested field like `observation[notes]` # --- # break_name:: Break a taxon name at the author # small_author:: Wrap the author in a span @@ -546,6 +548,12 @@ def as_displayed strip_html.unescape_html.strip_squeeze end + # Rails generates an id for a nested field like "foo[bar]" that's snake_case + # - no brackets. This gets you that string. (used in forms_helper) + def id_of_nested_field + gsub(/[\[\]]+/, "_").chop + end + # Insert a line break between the scientific name and the author # (for styling taxonomic names legibly) def break_name diff --git a/app/helpers/autocompleter_helper.rb b/app/helpers/autocompleter_helper.rb index 309c480701..b726e14c9a 100644 --- a/app/helpers/autocompleter_helper.rb +++ b/app/helpers/autocompleter_helper.rb @@ -25,7 +25,7 @@ def autocompleter_field(**args) ac_args[:wrap_data] = { autocompleter_target: "wrap" } ac_args[:label_after] = autocompleter_label_after(args) ac_args[:label_end] = autocompleter_label_end(args) - ac_args[:append] = autocompleter_dropdown + ac_args[:append] = autocompleter_append(args) tag.div(id: args[:controller_id], data: autocompleter_controller_data(args)) do @@ -61,8 +61,8 @@ def autocompleter_label_after(args) [ autocompleter_has_id_indicator, autocompleter_find_button(args), - autocompleter_keep_button(args), - autocompleter_hidden_field(**args) + autocompleter_keep_box_button(args), + autocompleter_edit_box_button(args) ].safe_join end end @@ -85,8 +85,9 @@ def autocompleter_create_button(args) icon_link_to( args[:create_text], "#", + id: "create_#{args[:type]}_btn", class: "ml-3 create-button", icon: :plus, show_text: true, icon_class: "text-primary", - name: "create_#{args[:type]}", class: "ml-3 create-button", + name: "create_#{args[:type]}", data: { autocompleter_target: "createBtn", action: "autocompleter#swapCreate:prevent" } ) @@ -110,22 +111,33 @@ def autocompleter_find_button(args) icon_link_to( args[:find_text], "#", icon: :find_on_map, show_text: false, icon_class: "text-primary", - name: "find_#{args[:type]}", class: "ml-3 d-none", + name: "find_#{args[:type]}", class: "ml-3 find-btn d-none", data: { map_target: "showBoxBtn", action: "map#showBox:prevent" } ) end - def autocompleter_keep_button(args) + def autocompleter_keep_box_button(args) return unless args[:keep_text] icon_link_to( args[:keep_text], "#", icon: :apply, show_text: false, icon_class: "text-primary", - active_icon: :edit, active_content: args[:edit_text], - name: "keep_#{args[:type]}", class: "ml-3 d-none", + name: "keep_#{args[:type]}", class: "ml-3 keep-btn d-none", data: { autocompleter_target: "keepBtn", map_target: "lockBoxBtn", - action: "map#toggleBoxLock:prevent" } + action: "map#toggleBoxLock:prevent form-exif#showFields" } + ) + end + + def autocompleter_edit_box_button(args) + return unless args[:keep_text] + + icon_link_to( + args[:edit_text], "#", + icon: :edit, show_text: false, icon_class: "text-primary", + name: "edit_#{args[:type]}", class: "ml-3 edit-btn d-none", + data: { autocompleter_target: "editBtn", map_target: "editBoxBtn", + action: "map#toggleBoxLock:prevent form-exif#showFields" } ) end @@ -150,6 +162,11 @@ def autocompleter_type_to_model(type) end end + def autocompleter_append(args) + [autocompleter_dropdown, + autocompleter_hidden_field(**args)].safe_join + end + def autocompleter_dropdown tag.div(class: "auto_complete dropdown-menu", data: { autocompleter_target: "pulldown", diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index ee748fcf9e..4cc398a318 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -503,7 +503,11 @@ def check_for_help_block(args) return args end - id = "#{args[:form].object_name}_#{args[:field]}_help" + id = [ + args[:form].object_name.to_s.id_of_nested_field, + args[:field].to_s, + "help" + ].join("_") args[:between] = capture do concat(args[:between]) concat(collapse_info_trigger(id)) diff --git a/app/helpers/panel_helper.rb b/app/helpers/panel_helper.rb index 5f265b4bc2..c6ca7e4cc4 100644 --- a/app/helpers/panel_helper.rb +++ b/app/helpers/panel_helper.rb @@ -168,7 +168,7 @@ def help_block(element = :div, string = "", **args, &block) # draw a help block with an arrow def help_block_with_arrow(direction = nil, **args, &block) - div_class = "well well-sm help-block position-relative" + div_class = "well well-sm mb-3 help-block position-relative" div_class += " mt-3" if direction == "up" tag.div(class: div_class, id: args[:id]) do @@ -182,7 +182,7 @@ def help_block_with_arrow(direction = nil, **args, &block) end def collapse_help_block(direction = nil, string = nil, **args, &block) - div_class = "well well-sm help-block position-relative" + div_class = "well well-sm mb-3 help-block position-relative" div_class += " mt-3" if direction == "up" content = block ? capture(&block) : string diff --git a/app/helpers/tabs/herbaria_helper.rb b/app/helpers/tabs/herbaria_helper.rb index 29c4a812d6..2adbb0f957 100644 --- a/app/helpers/tabs/herbaria_helper.rb +++ b/app/helpers/tabs/herbaria_helper.rb @@ -71,12 +71,12 @@ def herbaria_curator_request_tabs(herbarium:) end def new_herbarium_tab - [:create_herbarium.t, add_query_param(new_herbarium_path), + [:create_herbarium.l, add_query_param(new_herbarium_path), { class: tab_id(__method__.to_s) }] end def edit_herbarium_tab(herbarium) - [:edit_herbarium.t, + [:edit_herbarium.l, add_query_param(edit_herbarium_path(herbarium.id)), { class: tab_id(__method__.to_s) }] end diff --git a/app/helpers/tabs/observations_helper.rb b/app/helpers/tabs/observations_helper.rb index bc899e756c..f95255e391 100644 --- a/app/helpers/tabs/observations_helper.rb +++ b/app/helpers/tabs/observations_helper.rb @@ -199,7 +199,8 @@ def observations_download_as_csv_tab(query) # FORMS def observation_form_new_tabs - [new_herbarium_tab] + # [new_herbarium_tab] + [] end def observation_form_edit_tabs(obs:) diff --git a/app/javascript/application.js b/app/javascript/application.js index 98776d8d9f..e8e553e34a 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -13,11 +13,28 @@ import "@hotwired/turbo-rails" // form, or button like delete/patch: set data-turbo="true" to opt in // link_to with GET: set data-turbo-stream="true" to opt in Turbo.setFormMode("optin") -// https://stackoverflow.com/questions/77421369/turbo-response-to-render-javascript-alert/77434363#77434363 +// https://stackoverflow.com/a/77434363/3357635 // use: <%= turbo_stream.close_modal("modal_#{obs.id}_naming") %> Turbo.StreamActions.close_modal = function () { $("#" + this.templateContent.textContent).modal('hide') }; +// https://stackoverflow.com/a/76744968/3357635 +Turbo.StreamActions.update_input = function () { + this.targetElements.forEach((target) => { + target.value = this.templateContent.textContent + }); +}; +// https://stackoverflow.com/a/77836101/3357635 +Turbo.StreamActions.add_class = function () { + this.targetElements.forEach((target) => { + target.classList.add(this.templateContent.textContent) + }); +} +Turbo.StreamActions.remove_class = function () { + this.targetElements.forEach((target) => { + target.classList.remove(this.templateContent.textContent) + }); +} import "@rails/request.js" diff --git a/app/javascript/controllers/autocompleter_controller.js b/app/javascript/controllers/autocompleter_controller.js index 385a5abcdc..f81d551679 100644 --- a/app/javascript/controllers/autocompleter_controller.js +++ b/app/javascript/controllers/autocompleter_controller.js @@ -149,7 +149,7 @@ export default class extends Controller { // The select target is not the element, but a element is its target. static targets = ["input", "select", "pulldown", "list", "hidden", "wrap", - "createBtn", "hasIdIndicator", "keepBtn", "mapWrap", "collapseFields"] + "createBtn", "hasIdIndicator", "keepBtn", "editBtn", "mapWrap", "collapseFields"] static outlets = ["map"] initialize() { @@ -238,8 +238,8 @@ export default class extends Controller { this.last_fetch_params = ''; this.prepareInputElement(); this.prepareHiddenInput(); - if (!this.hasKeepBtnTarget || - !this.keepBtnTarget?.classList?.contains('active')) { + if (!this.hasEditBtnTarget || + this.editBtnTarget?.classList?.contains('d-none')) { this.clearHiddenId(); } this.constrainedSelectionUI(location); @@ -491,6 +491,7 @@ export default class extends Controller { // this.debug("ourChange(" + this.inputTarget.value + ")"); if (new_val.length == 0) { this.cssCollapseFields(); + this.clearHiddenId(); this.leaveCreate(); } else { this.cssUncollapseFields(); @@ -600,10 +601,11 @@ export default class extends Controller { this.verbose(this.inputTarget.value); this.old_value = this.inputTarget.value; // async, anything after this executes immediately + // STORE AND COMPARE SEARCH STRING. Otherwise we're doing double lookups if (this.hasGeocodeOutlet) { - this.geocodeOutlet.geolocatePlaceName(this.inputTarget.value); + this.geocodeOutlet.tryToGeolocate(this.inputTarget.value); } else if (this.hasMapOutlet) { - this.mapOutlet.geolocatePlaceName(this.inputTarget.value); + this.mapOutlet.tryToGeolocate(this.inputTarget.value); } // still necessary if primer unchanged, as likely? // this.populateMatches(); @@ -1018,6 +1020,8 @@ export default class extends Controller { // Respond to the state of the hidden input. Initially we may not have id, but // we also don't offer create until they've typed something. + // The `keepBtn` is for freezing the current box so people can pick a point. + // Otherwise you can't click a point inside the box. cssHasIdOrNo(hidden_id) { this.verbose("autocompleter:cssHasIdOrNo()"); @@ -1043,7 +1047,7 @@ export default class extends Controller { if (this.wrapTarget.classList.contains('create')) { this.mapWrapTarget.classList.remove('d-none'); } else { - this.mapWrapTarget.classList.add('d-none'); + // this.mapWrapTarget.classList.add('d-none'); } } } @@ -1075,14 +1079,15 @@ export default class extends Controller { { north, south, east, west } = this.hiddenTarget.dataset, hidden_data = { id: hidden_id, north, south, east, west }; - this.verbose("autocompleter:hidden_data: " + JSON.stringify(hidden_data)); // comparing data, not just ids, because google locations have same -1 id if (JSON.stringify(hidden_data) == JSON.stringify(this.stored_data)) { - this.verbose("autocompleter: hidden data did not change"); + this.verbose("autocompleter: hidden_data did not change"); } else { clearTimeout(this.data_timer); this.data_timer = setTimeout(() => { - this.verbose("autocompleter: hidden data changed"); + this.verbose("autocompleter: hidden_data changed"); + this.verbose("autocompleter:hidden_data: ") + this.verbose(JSON.stringify(hidden_data)); this.cssHasIdOrNo(hidden_id); if (this.hasKeepBtnTarget) { this.keepBtnTarget.classList.remove('active'); diff --git a/app/javascript/controllers/geocode_controller.js b/app/javascript/controllers/geocode_controller.js index 892b06ee9c..477d072046 100644 --- a/app/javascript/controllers/geocode_controller.js +++ b/app/javascript/controllers/geocode_controller.js @@ -45,6 +45,7 @@ export default class extends Controller { } tryToGeocode() { + this.verbose("geocode:tryToGeocode") const location = this.validateLatLngInputs(false) if (location && @@ -75,6 +76,7 @@ export default class extends Controller { } tryToGeolocate() { + this.verbose("geocode:tryToGeolocate") const address = this.placeInputTarget.value if (this.ignorePlaceInput === false && @@ -84,6 +86,8 @@ export default class extends Controller { } geolocatePlaceName(address) { + if (address == this.lastGeolocatedAddress) return false + this.lastGeolocatedAddress = address this.verbose("geocode:geolocatePlaceName") this.verbose(address) @@ -148,6 +152,8 @@ export default class extends Controller { // Format the address components for MO style. formatMOPlaceName(result) { + const ignore_types = ["postal_code", "postal_code_suffix", "street_number"] + let name_components = [], usa_location = false result.address_components.forEach((component) => { if (component.types.includes("country") && component.short_name == "US") { @@ -158,7 +164,7 @@ export default class extends Controller { component.long_name.includes("County")) { // MO uses "Co." for County name_components.push(component.long_name.replace("County", "Co.")) - } else if (component.types.includes("postal_code")) { + } else if (ignore_types.some((type) => component.types.includes(type))) { // skip it for all. non-US countries it's an important differentiator? } else { name_components.push(component.long_name) diff --git a/app/javascript/controllers/map_controller.js b/app/javascript/controllers/map_controller.js index b359df1823..da6b8ca127 100644 --- a/app/javascript/controllers/map_controller.js +++ b/app/javascript/controllers/map_controller.js @@ -12,7 +12,7 @@ export default class extends GeocodeController { "eastInput", "highInput", "lowInput", "placeInput", "locationId", "getElevation", "mapClearBtn", "controlWrap", "toggleMapBtn", "latInput", "lngInput", "altInput", "showBoxBtn", "lockBoxBtn", - "autocompleter"] + "editBoxBtn", "autocompleter"] connect() { this.element.dataset.stimulus = "map-connected" @@ -95,19 +95,18 @@ export default class extends GeocodeController { // Lock rectangle so it's not editable, and show this state in the icon link toggleBoxLock(event) { - if (this.rectangle && this.hasLockBoxBtnTarget) { + if (this.rectangle && this.hasLockBoxBtnTarget && + this.hasEditBoxBtnTarget) { if (this.rectangle.getEditable() === true) { this.rectangle.setEditable(false) this.rectangle.setOptions({ clickable: false }) - this.lockBoxBtnTarget.classList.add("active") - const active_title = this.lockBoxBtnTarget.dataset?.activeTitle ?? '' - this.lockBoxBtnTarget.setAttribute("title", active_title) + this.lockBoxBtnTarget.classList.add("d-none") + this.editBoxBtnTarget.classList.remove("d-none") } else { this.rectangle.setEditable(true) this.rectangle.setOptions({ clickable: true }) - this.lockBoxBtnTarget.classList.remove("active") - const title = this.lockBoxBtnTarget.dataset?.title ?? '' - this.lockBoxBtnTarget.setAttribute("title", title) + this.lockBoxBtnTarget.classList.remove("d-none") + this.editBoxBtnTarget.classList.add("d-none") } } } @@ -116,6 +115,7 @@ export default class extends GeocodeController { // If we only have one marker, don't use fitBounds - it's too zoomed in. // Call setCenter, setZoom with marker position and desired zoom level. drawMap() { + this.verbose("map:drawMap") this.map = new google.maps.Map(this.mapDivTarget, this.mapOptions) if (this.mapBounds) { if (Object.keys(this.collection.sets).length == 1) { @@ -141,6 +141,7 @@ export default class extends GeocodeController { buildOverlays() { if (!this.collection) return + this.verbose("map:buildOverlays") for (const [_xywh, set] of Object.entries(this.collection.sets)) { // this.verbose({ set }) // NOTE: according to the MapSet class, location sets are always is_box. @@ -401,6 +402,7 @@ export default class extends GeocodeController { } else if (["location", "hybrid"].includes(this.map_type)) { // Only geocode lat/lng if we have no location_id and not ignoring place // ...and only geolocate placeName if we have no lat/lng + // Note: is this the right logic ????????????? if (this.ignorePlaceInput !== false) { this.tryToGeocode() // multiple possible results } else { @@ -415,6 +417,7 @@ export default class extends GeocodeController { if (!this.hasLocationIdTarget || !this.locationIdTarget.dataset.north) return false + this.verbose("map:mapLocationIdData") const bounds = { north: parseFloat(this.locationIdTarget.dataset.north), south: parseFloat(this.locationIdTarget.dataset.south), @@ -436,22 +439,22 @@ export default class extends GeocodeController { const east = parseFloat(this.eastInputTarget.value) const west = parseFloat(this.westInputTarget.value) - if (!(isNaN(north) || isNaN(south) || isNaN(east) || isNaN(west))) { - this.verbose("map:calculateRectangle") - const bounds = { north: north, south: south, east: east, west: west } - if (this.rectangle) { - this.rectangle.setBounds(bounds) - } - this.map.fitBounds(bounds) + if (isNaN(north) || isNaN(south) || isNaN(east) || isNaN(west)) return false + + this.verbose("map:calculateRectangle") + const bounds = { north: north, south: south, east: east, west: west } + if (this.rectangle) { + this.rectangle.setBounds(bounds) } + this.map.fitBounds(bounds) } // Infers a rectangle from the google place, if found. (could be point/bounds) placeClosestRectangle(viewport, extents) { + this.verbose("map:placeClosestRectangle") // Prefer extents for rectangle, fallback to viewport let bounds = extents || viewport if (bounds != undefined && bounds?.north) { - this.verbose("map:placeClosestRectangle") this.placeRectangle(bounds) } // else if (center) { @@ -466,6 +469,7 @@ export default class extends GeocodeController { // called by toggleMap checkForMarker() { + this.verbose("map:checkForMarker") let center if (center = this.validateLatLngInputs(false)) { this.calculateMarker({ detail: { request_params: center } }) @@ -478,11 +482,11 @@ export default class extends GeocodeController { // so, drops a pin on that location and center. Otherwise, checks if place // input has been prepopulated and uses that to focus map and drop a marker. calculateMarker(event) { - this.verbose("map:calculateMarker") if (this.map == undefined || !this.hasLatInputTarget || this.latInputTarget.value === '' || this.lngInputTarget.value === '') return false + this.verbose("map:calculateMarker") let location if (event?.detail?.request_params) { location = event.detail.request_params @@ -501,27 +505,38 @@ export default class extends GeocodeController { toggleMap() { this.verbose("map:toggleMap") if (this.opened) { - this.opened = false - this.controlWrapTarget.classList.remove("map-open") + this.closeMap() } else { - this.opened = true - this.controlWrapTarget.classList.add("map-open") + this.openMap() + } + } - if (this.map == undefined) { - this.drawMap() - this.makeMapClickable() - } else if (this.mapBounds) { - this.map.fitBounds(this.mapBounds) - } + closeMap() { + this.verbose("map:closeMap") + this.opened = false + this.controlWrapTarget.classList.remove("map-open") + } - setTimeout(() => { - this.checkForMarker() - this.checkForBox() // regardless if point - }, 500) // wait for map to open + openMap() { + this.verbose("map:openMap") + this.opened = true + this.controlWrapTarget.classList.add("map-open") + + if (this.map == undefined) { + this.drawMap() + this.makeMapClickable() + } else if (this.mapBounds) { + this.map.fitBounds(this.mapBounds) } + + setTimeout(() => { + this.checkForMarker() + this.checkForBox() // regardless if point + }, 500) // wait for map to open } makeMapClickable() { + this.verbose("map:makeMapClickable") google.maps.event.addListener(this.map, 'click', (e) => { // this.map.addListener('click', (e) => { const location = e.latLng.toJSON() diff --git a/app/views/controllers/herbaria/_form.erb b/app/views/controllers/herbaria/_form.erb new file mode 100644 index 0000000000..65527c4098 --- /dev/null +++ b/app/views/controllers/herbaria/_form.erb @@ -0,0 +1,122 @@ +<%# locals: (action:, local: true) -%> +<% +create = (action == :create) +button_name = create ? :CREATE.l : :SAVE.l +help = ac_help = nil +if @herbarium.personal_user_id == @user.id + help = :edit_herbarium_this_is_personal_herbarium.tp +end +if in_admin_mode? && !create + top_users = herbarium_top_users(@herbarium.id) + if top_users.empty? + admin_help = :edit_herbarium_no_herbarium_records.l + else + admin_help = capture do + top_users.each do |name, login, count| + concat(tag.div(:edit_herbarium_user_records.t( + name: "#{name} (#{login})", num: count + ))) + end + end + end +end + +form_args = { + model: @herbarium, + id: "herbarium_form", + data: { + controller: "map", map_open: false, + map_autocompleter_outlet: "#herbarium_location_autocompleter" + } +} +if local == true + form_args = form_args.merge({ local: true }) +else + form_args = form_args.deep_merge({ data: { turbo: true } }) +end + +%> + +<%= form_with(**form_args) do |f| %> + + <%= f.hidden_field(:back, value: @back) %> + <%= f.hidden_field(:q, value: get_query_param) %> + + <%= text_field_with_label(form: f, field: :name, label: :NAME.t + ":", + between: :required, help:) %> + + <% if in_admin_mode? %> + + <%= autocompleter_field(form: f, field: :personal_user_name, type: :user, + label: :edit_herbarium_admin_make_personal.t, + help: admin_help, inline: true) %> + + <% elsif action == :create || @herbarium.can_make_personal?(@user) %> + + <%= check_box_with_label( + form: f, field: :personal, label: :create_herbarium_personal.l, + help: :create_herbarium_personal_help.t( + name: @user.personal_herbarium_name + ) + ) %> + + <% end %> + + <%= submit_button(form: f, button: button_name, center: true) %> + + <% if !@herbarium.personal_user_id %> + <%= text_field_with_label(form: f, field: :code, size: 8, inline: true, + label: :create_herbarium_code.l + ":", + help: :create_herbarium_code_help.t, + between: :optional) %> + <% end %> + + + <% append = capture do + tag.div(class: "mb-5 d-none", data: { autocompleter_target: "mapWrap" }) do + render(partial: "shared/form_location_map", + locals: { id: "herbarium_form_map", map_type: "observation" }) + end + end %> + + + <%= autocompleter_field( + form: f, field: :place_name, type: :location, + label: [tag.span("#{:LOCATION.l}:", class: "unconstrained-label"), + tag.span("#{:form_observations_create_locality.l}:", + class: "create-label")].safe_join(" "), + controller_data: { map_target: "autocompleter" }, + controller_id: "herbarium_location_autocompleter", + between: :optional, + append:, + hidden_data: { map_target: "locationId" }, + create_text: :form_observations_create_locality.l, + map_outlet: "#herbarium_form", + data: { + map_target: "placeInput", + # action: [ + # "map:pointChanged@window->autocompleter#swap", + # "map:googlePrimer@window->autocompleter#refreshGooglePrimer" + # ] + } + ) %> + + <%= render(partial: "locations/form/bounds_hidden_fields", + locals: { location: @location, target_controller: :map }) %> + + + <%= text_field_with_label(form: f, field: :email, + label: :create_herbarium_email.l + ":", + between: :optional) %> + + <%= text_area_with_label(form: f, field: :mailing_address, rows: 5, + label: :create_herbarium_mailing_address.l + ":", + between: :optional) %> + + <%= text_area_with_label(form: f, field: :description, rows: 10, + label: :NOTES.l + ":", + between: :optional) %> + + <%= submit_button(form: f, button: button_name, center: true) %> + +<% end %> diff --git a/app/views/controllers/herbaria/_form.html.erb b/app/views/controllers/herbaria/_form.html.erb deleted file mode 100644 index da589eb12e..0000000000 --- a/app/views/controllers/herbaria/_form.html.erb +++ /dev/null @@ -1,70 +0,0 @@ -<%= form_with(model: @herbarium, id: "herbarium_form") do |f| %> - - <%= submit_button(form: f, button: button_name.t, center: true) %> - - <%= f.hidden_field :back, value: @back %> - <%= f.hidden_field :q, value: get_query_param %> - - <%= text_field_with_label(form: f, field: :name, label: :NAME.t + ":", - between: :required) %> - - <% if in_admin_mode? %> - <%= autocompleter_field(form: f, field: :personal_user_name, type: :user, - label: :edit_herbarium_admin_make_personal.t, - inline: true) %> - - <% if button_name != :CREATE %> - <%= help_block_with_arrow("up") do %> - <% top_users = herbarium_top_users(@herbarium.id) - top_users.each do |name, login, count| %> - <%= :edit_herbarium_user_records.t( - name: "#{name} (#{login})", num: count - ) %>
- <% end %> - <%= :edit_herbarium_no_herbarium_records.t if top_users.empty? %> - <% end %> - <% end %> - - <% else %> - <% if @herbarium.personal_user_id == @user.id %> - <%= content_tag(:div, class: "form-group") do - help_block(:div, :edit_herbarium_this_is_personal_herbarium.tp) - end %> - <% end %> - - <% if button_name == :CREATE || @herbarium.can_make_personal?(@user) %> - <%= check_box_with_label(form: f, field: :personal, - label: :create_herbarium_personal.t) %> - <%= help_block_with_arrow("up") do %> - <%= :create_herbarium_personal_help.t( - name: @user.personal_herbarium_name - ) %> - <% end %> - <% end %> - <% end %> - - <% if !@herbarium.personal_user_id %> - <%= text_field_with_label(form: f, field: :code, size: 8, inline: true, - label: :create_herbarium_code.t + ":", - between: :optional) %> - <%= help_block_with_arrow("up") do :create_herbarium_code_help.t end %> - <% end %> - - <%= autocompleter_field(form: f, field: :place_name, type: :location, - label: :LOCATION.t + ":", between: :optional) %> - - <%= text_field_with_label(form: f, field: :email, - label: :create_herbarium_email.t + ":", - between: :optional) %> - - <%= text_area_with_label(form: f, field: :mailing_address, rows: 5, - label: :create_herbarium_mailing_address.t + ":", - between: :optional) %> - - <%= text_area_with_label(form: f, field: :description, rows: 10, - label: :NOTES.t + ":", - between: :optional) %> - - <%= submit_button(form: f, button: button_name.t, center: true) %> - -<% end %> diff --git a/app/views/controllers/herbaria/_update_observation.erb b/app/views/controllers/herbaria/_update_observation.erb new file mode 100644 index 0000000000..3dbd03a484 --- /dev/null +++ b/app/views/controllers/herbaria/_update_observation.erb @@ -0,0 +1,13 @@ +<%# Close the modal, update the obs with the new herbarium, and flash %> +<%= turbo_stream.close_modal("modal_herbarium") %> +<%= turbo_stream.remove("modal_herbarium") %> + +<%= turbo_stream.update("page_flash") { flash_notices_html } %> + +<%= turbo_stream.update_input("herbarium_record_herbarium_name", + @herbarium.name) %> + +<%= turbo_stream.update_input("herbarium_record_herbarium_id", + @herbarium.id) %> + +<%= turbo_stream.remove("create_herbarium_btn") %> diff --git a/app/views/controllers/herbaria/edit.html.erb b/app/views/controllers/herbaria/edit.html.erb index 48c39fa337..b498c0cd14 100644 --- a/app/views/controllers/herbaria/edit.html.erb +++ b/app/views/controllers/herbaria/edit.html.erb @@ -4,4 +4,5 @@ add_page_title(:edit_herbarium_title.l) add_tab_set(herbarium_form_edit_tabs(herbarium: @herbarium)) %> -<%= render(partial: "herbaria/form", locals: { button_name: :SAVE }) %> +<%= render(partial: "herbaria/form", + locals: { action: :update, local: true }) %> diff --git a/app/views/controllers/herbaria/new.html.erb b/app/views/controllers/herbaria/new.html.erb index 81714fe2b5..8076abbbfb 100644 --- a/app/views/controllers/herbaria/new.html.erb +++ b/app/views/controllers/herbaria/new.html.erb @@ -5,4 +5,5 @@ add_page_title(:create_herbarium_title.l) add_tab_set(herbarium_form_new_tabs) %> -<%= render(partial: "herbaria/form", locals: { button_name: :CREATE }) %> +<%= render(partial: "herbaria/form", + locals: { action: :create, local: true }) %> diff --git a/app/views/controllers/herbaria/show.html.erb b/app/views/controllers/herbaria/show.html.erb index c0e87289e8..3c22f59c9b 100644 --- a/app/views/controllers/herbaria/show.html.erb +++ b/app/views/controllers/herbaria/show.html.erb @@ -4,7 +4,7 @@ add_page_title(@herbarium.format_name.t) add_pager_for(@herbarium) add_tab_set(herbarium_show_tabs(herbarium: @herbarium, user: @user)) -map = @herbarium.location ? true : false +map = @herbarium.location @container = :wide %> @@ -20,8 +20,10 @@ map = @herbarium.location ? true : false
- <%= render(partial: "herbaria/curator_table", - locals: { herbarium: @herbarium }) %> + <% if @herbarium.curators.present? %> + <%= render(partial: "herbaria/curator_table", + locals: { herbarium: @herbarium }) %> + <% end %> <% if @herbarium.curator?(@user) || in_admin_mode? %> @@ -49,14 +51,14 @@ map = @herbarium.location ? true : false <% end %>
- <% if !@herbarium.description.blank? %> + <% if @herbarium.description.present? %>
<%= :NOTES.t %>:
<%= @herbarium.description.tpl %>
<% end %> - <% if @herbarium.mailing_address && !@herbarium.mailing_address.empty? %> + <% if @herbarium.mailing_address.present? %>
<%= :herbarium_mailing_address.t %>:
<%= @herbarium.mailing_address.tp %> @@ -66,7 +68,10 @@ map = @herbarium.location ? true : false <% if map %>
- <%= make_map(objects: [@herbarium.location]) %> + <%= tag.div(class: "mb-3") { make_map(objects: [@herbarium.location]) } %> + <%= tag.p(id: "herbarium_location") do + "#{:LOCATION.l}: #{@herbarium.location.text_name}" + end %>
<% end %>
diff --git a/app/views/controllers/observations/form/specimen/_herbarium_record.html.erb b/app/views/controllers/observations/form/specimen/_herbarium_record.html.erb index 8504c75967..0851b92772 100644 --- a/app/views/controllers/observations/form/specimen/_herbarium_record.html.erb +++ b/app/views/controllers/observations/form/specimen/_herbarium_record.html.erb @@ -6,7 +6,9 @@ form: fhr, field: :herbarium_name, type: :herbarium, value: @herbarium_name, hidden_value: @herbarium_id, label: "#{:herbarium_record_herbarium_name.t}:", - help: :form_observations_herbarium_record_help.t + help: :form_observations_herbarium_record_help.t, + create_text: :create_herbarium.l, create: "herbarium", + create_path: new_herbarium_path ) %> <%= text_field_with_label( form: fhr, field: :accession_number, value: @accession_number, diff --git a/config/initializers/turbo_stream_actions.rb b/config/initializers/turbo_stream_actions.rb index fa3d929d6a..0d06e8c33e 100644 --- a/config/initializers/turbo_stream_actions.rb +++ b/config/initializers/turbo_stream_actions.rb @@ -1,11 +1,23 @@ # frozen_string_literal: true -# https://stackoverflow.com/questions/77421369/turbo-response-to-render-javascript-alert/77434363#77434363 +# https://stackoverflow.com/a/77434363/3357635 # this is optional but makes it much cleaner module CustomTurboStreamActions def close_modal(id) action(:close_modal, "#", id) end + def update_input(id, value) + action(:update_input, id, value) + end + + def add_class(id, class_name) + action(:add_class, id, class_name) + end + + def remove_class(id, class_name) + action(:remove_class, id, class_name) + end + ::Turbo::Streams::TagBuilder.include(self) end diff --git a/config/locales/en.txt b/config/locales/en.txt index fc7438f47c..d71243de76 100644 --- a/config/locales/en.txt +++ b/config/locales/en.txt @@ -699,6 +699,8 @@ close: close CREATE: Create create: create + DEFINE: Define + define: define DELETE: Delete delete: delete # deprecate a name @@ -771,16 +773,16 @@ destroyed: destroyed FILTERED: Filtered filtered: filtered + IGNORING: Ignoring + ignoring: ignoring MODIFIED: Modified modified: modified + NEW: New + new: new OPTIONAL: Optional optional: optional REQUIRED: Required required: required - WATCHING: Watching - watching: watching - IGNORING: Ignoring - ignoring: ignoring TRACKING: Tracking tracking: tracking UPDATED: Updated @@ -789,6 +791,8 @@ updated_at: updated at VIEWED: Viewed viewed: viewed + WATCHING: Watching + watching: watching # Common adjectives describing species names. (Note: "approved" seems to be # interchangeable with "accepted" in our site -- both indicate that the @@ -1670,10 +1674,10 @@ form_observations_open_map: "[:SHOW] [:map]" form_observations_hide_map: "[:HIDE] [:map]" form_observations_clear_map: "[:CLEAR] [:location]" - form_observations_click_point: "Tip: select a locality to center the map, then click a point on the map to set a location." + form_observations_click_point: "Tip: select a locality to center the map, then click a point on the map to set a location. If you're defining a new locality, click \"Use these bounds\" so you can select a point within." form_observations_locality_contains: "[:LOCALITIES] containing this point" - form_observations_create_locality: "[:CREATE] [:locality]" - form_observations_use_locality: "[:USE] this [:locality]" + form_observations_create_locality: "[:NEW] [:locality]" + form_observations_use_locality: "Use these bounds and select a point within" form_observations_edit_locality: "[:EDIT] this [:locality]" form_observations_notes_help: Please include any additional information you can think of about this observation that isn't clear from the photographs, e.g., habitat, substrate or nearby trees; distinctive texture, scent, taste, staining or bruising; results of chemical or microscopic analyses, etc. form_observations_remove_image_confirm: Are you sure you want to remove this image? This will only remove this image from this observation. If it is attached to other observations, it will remain attached to them. @@ -2607,8 +2611,8 @@ show_herbarium_request_sent: Request has been sent to admins. We'll get back to you as soon as possible. # herbaria/create - create_herbarium: Create New Fungarium - create_herbarium_title: Create New Fungarium + create_herbarium: New Fungarium + create_herbarium_title: New Fungarium create_herbarium_personal: Check this box if this is your personal fungarium create_herbarium_personal_help: Each user can have one personal fungarium that they have curator privileges for. By default it is called "[name]", but you can change that name to anything you like. create_herbarium_code: Standard Abbreviation @@ -3086,7 +3090,7 @@ # info/textile sandbox_enter: Enter Textile you wish to test here - sandbox_header: This page let's you play with the textile markup language. Click 'Test' to see what your markup looks like. You will need to copy the results from this page to where ever you want to use it. + sandbox_header: This page lets you play with the textile markup language. Click 'Test' to see what your markup looks like. You will need to copy the results from this page to wherever you want to use it. sandbox_link_hobix_textile_reference: hobix Textile Reference sandbox_link_hobix_textile_cheatsheet: hobix cheatsheet (Textile Quick Reference) sandbox_link_textile_language_website: Textile Language website diff --git a/test/system/herbarium_form_system_test.rb b/test/system/herbarium_form_system_test.rb new file mode 100644 index 0000000000..3f5dedd452 --- /dev/null +++ b/test/system/herbarium_form_system_test.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require("application_system_test_case") + +class HerbariumFormSystemTest < ApplicationSystemTestCase + def test_create_fungarium_new_location + # browser = page.driver.browser + rolf = users("rolf") + login!(rolf) + + visit("/herbaria/new") + assert_selector("body.herbaria__new") + create_herbarium_with_new_location + + # assert_no_selector("#modal_herbarium") + assert_selector("body.herbaria__show") + assert_selector("h1", text: "Herbarium des Cévennes (CEV)") + assert_selector("#herbarium_location", + text: "Génolhac, Gard, Occitanie, France") + end + + def test_observation_form_create_fungarium_new_location + rolf = users("rolf") + login!(rolf) + + visit("/observations/new") + assert_selector("body.observations__new") + + assert_selector("#observation_naming_specimen") + scroll_to(find("#observation_naming_specimen"), align: :top) + check("observation_specimen") + assert_selector("#herbarium_record_herbarium_name") + assert_selector(".create-link", text: :create_herbarium.l) + click_link(:create_herbarium.l) + + assert_selector("#modal_herbarium") + create_herbarium_with_new_location + + assert_no_selector("#modal_herbarium") + assert_field("herbarium_record_herbarium_name", + with: "Herbarium des Cévennes") + end + + def create_herbarium_with_new_location + assert_selector("#herbarium_place_name") + fill_in("herbarium_place_name", with: "genohlac gard france") + assert_link(:form_observations_create_locality.l) + click_link(:form_observations_create_locality.l) + + assert_selector("#herbarium_place_name.geocoded") + assert_field("herbarium_place_name", + with: "Génolhac, Gard, Occitanie, France") + + assert_field("herbarium_location_id", with: "-1", type: :hidden) + assert_field("location_north", with: "44.3726", type: :hidden) + assert_field("location_east", with: "3.985", type: :hidden) + assert_field("location_south", with: "44.3055", type: :hidden) + assert_field("location_west", with: "3.9113", type: :hidden) + assert_field("location_high", with: "1388.2098", type: :hidden) + assert_field("location_low", with: "287.8201", type: :hidden) + + within("#herbarium_form") do + fill_in("herbarium_name", with: "Herbarium des Cévennes") + fill_in("herbarium_code", with: "CEV") + click_commit + end + end +end