neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
some better things
ac18dca · gmorell · 2025-07-01 21:55
Files changed
modified
apps/room_sanctum/assets/js/app.js
+190
−15
@@ -26,9 +26,9 @@ import {Socket} from "phoenix"
| 26 | 26 | import {LiveSocket} from "phoenix_live_view" |
| 27 | 27 | import topbar from "../vendor/topbar" |
| 28 | 28 | |
| 29 | −import './leaflet/leaflet-map' | |
| 30 | −import './leaflet/leaflet-marker' | |
| 31 | −import './leaflet/leaflet-icon' | |
| 29 | +// import './leaflet/leaflet-map' | |
| 30 | +// import './leaflet/leaflet-marker' | |
| 31 | +// import './leaflet/leaflet-icon' | |
| 32 | 32 | |
| 33 | 33 | import L from 'leaflet' |
| 34 | 34 | import 'leaflet-centermarker' |
@@ -48,31 +48,169 @@ Hooks.MapPush = {
| 48 | 48 | Hooks.mkMap = { |
| 49 | 49 | mounted() { |
| 50 | 50 | const markers = {} |
| 51 | − latlng = document.getElementById('map').getAttribute('data-latlng') | |
| 51 | + let searchMarker = null; | |
| 52 | + | |
| 53 | + let latlng = document.getElementById('map').getAttribute('data-latlng') | |
| 52 | 54 | if (latlng != null) { |
| 53 | − coords = JSON.parse(latlng) | |
| 55 | + let coords = JSON.parse(latlng) | |
| 54 | 56 | } else { |
| 55 | − coords = [42.3736, -71.1097] | |
| 57 | + let coords = [42.3736, -71.1097] | |
| 56 | 58 | } |
| 57 | − coords = [18.33967, -67.23713] | |
| 59 | + | |
| 58 | 60 | var map = L.map('map', {keyboard: true}).setView(coords, 13); |
| 59 | 61 | |
| 60 | 62 | const view = this; |
| 61 | − var marker = L.centerMarker(map).on("newposition", function() { | |
| 62 | − var latlng = marker.getLatLng() | |
| 63 | + var centerMarker = L.centerMarker(map).on("newposition", function() { | |
| 64 | + var latlng = centerMarker.getLatLng() | |
| 63 | 65 | console.log("New position: " + latlng.lat + ", " + latlng.lng); |
| 64 | 66 | view.pushEventTo("#foci-form", "map-update", { latlng: latlng }); |
| 67 | + }); | |
| 68 | + centerMarker.addTo(map); | |
| 65 | 69 | |
| 66 | − });; | |
| 67 | − marker.addTo(map); | |
| 68 | − | |
| 69 | − L.tileLayer('https:{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
| 70 | + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
| 70 | 71 | maxZoom: 19, |
| 71 | − attribution: '© <a href="https:openstreetmap.org/copyright">OpenStreetMap contributors</a>' | |
| 72 | + attribution: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>' | |
| 72 | 73 | }).addTo(map); |
| 73 | 74 | |
| 75 | + // Search functionality | |
| 76 | + const searchInput = document.getElementById('location-search'); | |
| 77 | + const searchResults = document.getElementById('search-results'); | |
| 78 | + let searchTimeout; | |
| 79 | + | |
| 80 | + const performSearch = async (query) => { | |
| 81 | + if (query.length < 3) { | |
| 82 | + searchResults.classList.add('hidden'); | |
| 83 | + return; | |
| 84 | + } | |
| 85 | + | |
| 86 | + try { | |
| 87 | + const response = await fetch( | |
| 88 | + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&addressdetails=1` | |
| 89 | + ); | |
| 90 | + const results = await response.json(); | |
| 91 | + | |
| 92 | + displaySearchResults(results); | |
| 93 | + } catch (error) { | |
| 94 | + console.error('Search error:', error); | |
| 95 | + searchResults.classList.add('hidden'); | |
| 96 | + } | |
| 97 | + }; | |
| 98 | + | |
| 99 | + const displaySearchResults = (results) => { | |
| 100 | + searchResults.innerHTML = ''; | |
| 101 | + | |
| 102 | + if (results.length === 0) { | |
| 103 | + searchResults.innerHTML = '<div class="p-3 text-gray-500">No results found</div>'; | |
| 104 | + searchResults.classList.remove('hidden'); | |
| 105 | + return; | |
| 106 | + } | |
| 107 | + | |
| 108 | + results.forEach(result => { | |
| 109 | + const resultDiv = document.createElement('div'); | |
| 110 | + resultDiv.className = 'p-3 hover:bg-gray-100 cursor-pointer border-b border-gray-200 last:border-b-0'; | |
| 111 | + resultDiv.innerHTML = ` | |
| 112 | + <div class="font-medium">${result.display_name}</div> | |
| 113 | + <div class="text-sm text-gray-600">${result.type} • ${result.class}</div> | |
| 114 | + `; | |
| 115 | + | |
| 116 | + resultDiv.addEventListener('click', () => { | |
| 117 | + selectSearchResult(result); | |
| 118 | + }); | |
| 119 | + | |
| 120 | + searchResults.appendChild(resultDiv); | |
| 121 | + }); | |
| 122 | + | |
| 123 | + searchResults.classList.remove('hidden'); | |
| 124 | + }; | |
| 125 | + | |
| 126 | + const selectSearchResult = (result) => { | |
| 127 | + const lat = parseFloat(result.lat); | |
| 128 | + const lon = parseFloat(result.lon); | |
| 129 | + | |
| 130 | + // Update search input | |
| 131 | + searchInput.value = result.display_name; | |
| 132 | + searchResults.classList.add('hidden'); | |
| 133 | + | |
| 134 | + // Remove previous search marker | |
| 135 | + if (searchMarker) { | |
| 136 | + map.removeLayer(searchMarker); | |
| 137 | + } | |
| 138 | + | |
| 139 | + // Add new search marker | |
| 140 | + searchMarker = L.marker([lat, lon], { | |
| 141 | + icon: L.icon({ | |
| 142 | + iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png', | |
| 143 | + shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', | |
| 144 | + iconSize: [25, 41], | |
| 145 | + iconAnchor: [12, 41], | |
| 146 | + popupAnchor: [1, -34], | |
| 147 | + shadowSize: [41, 41] | |
| 148 | + }) | |
| 149 | + }).addTo(map); | |
| 150 | + | |
| 151 | + // Add popup with result info | |
| 152 | + searchMarker.bindPopup(` | |
| 153 | + <div> | |
| 154 | + <strong>${result.display_name}</strong><br> | |
| 155 | + <small>${result.type} • ${result.class}</small><br> | |
| 156 | + <button onclick="window.selectThisLocation(${lat}, ${lon})" class="btn btn-sm btn-primary mt-2"> | |
| 157 | + Select This Location | |
| 158 | + </button> | |
| 159 | + </div> | |
| 160 | + `).openPopup(); | |
| 161 | + | |
| 162 | + // Pan to the result | |
| 163 | + map.setView([lat, lon], 16); | |
| 164 | + }; | |
| 165 | + | |
| 166 | + // Global function to select a location from popup | |
| 167 | + window.selectThisLocation = (lat, lon) => { | |
| 168 | + // Move center marker to selected location | |
| 169 | + centerMarker.setLatLng([lat, lon]); | |
| 170 | + map.setView([lat, lon], 16); | |
| 171 | + | |
| 172 | + // Trigger the position update | |
| 173 | + view.pushEventTo("#foci-form", "map-update", {latlng: {lat: lat, lng: lon}}); | |
| 174 | + | |
| 175 | + // Close popup | |
| 176 | + if (searchMarker) { | |
| 177 | + searchMarker.closePopup(); | |
| 178 | + } | |
| 179 | + }; | |
| 180 | + | |
| 181 | + // Search input event listeners | |
| 182 | + if (searchInput) { | |
| 183 | + searchInput.addEventListener('input', (e) => { | |
| 184 | + clearTimeout(searchTimeout); | |
| 185 | + const query = e.target.value.trim(); | |
| 186 | + | |
| 187 | + if (query.length === 0) { | |
| 188 | + searchResults.classList.add('hidden'); | |
| 189 | + return; | |
| 190 | + } | |
| 191 | + | |
| 192 | + searchTimeout = setTimeout(() => { | |
| 193 | + performSearch(query); | |
| 194 | + }, 300); | |
| 195 | + }); | |
| 196 | + | |
| 197 | + // Hide search results when clicking outside | |
| 198 | + document.addEventListener('click', (e) => { | |
| 199 | + if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { | |
| 200 | + searchResults.classList.add('hidden'); | |
| 201 | + } | |
| 202 | + }); | |
| 203 | + | |
| 204 | + // Show search results when focusing on input | |
| 205 | + searchInput.addEventListener('focus', () => { | |
| 206 | + if (searchInput.value.trim().length >= 3) { | |
| 207 | + performSearch(searchInput.value.trim()); | |
| 208 | + } | |
| 209 | + }); | |
| 210 | + } | |
| 211 | + | |
| 74 | 212 | this.handleEvent("add_marker", ({lat, lon}) => { |
| 75 | − const marker = L.marker(L.latLng(lat, lomn)) | |
| 213 | + const marker = L.marker(L.latLng(lat, lon)) | |
| 76 | 214 | marker.addTo(map) |
| 77 | 215 | }) |
| 78 | 216 | } |
@@ -112,6 +250,43 @@ Hooks.mkTesterMap = {
| 112 | 250 | } |
| 113 | 251 | } |
| 114 | 252 | |
| 253 | +Hooks.mkShowMap = { | |
| 254 | + latlng() { return this.el.dataset.latlng }, | |
| 255 | + mounted() { | |
| 256 | + var latlng = this.latlng() | |
| 257 | + console.log("Show Map Mounted") | |
| 258 | + console.log(latlng) | |
| 259 | + | |
| 260 | + let coords; | |
| 261 | + if (latlng == null) { | |
| 262 | + coords = [42.3736, -71.1097]; | |
| 263 | + } else { | |
| 264 | + coords = JSON.parse(latlng); | |
| 265 | + } | |
| 266 | + | |
| 267 | + // Create static map for show view with unique ID | |
| 268 | + let map = L.map(this.el.id, { | |
| 269 | + keyboard: false, | |
| 270 | + dragging: false, | |
| 271 | + touchZoom: false, | |
| 272 | + scrollWheelZoom: false, | |
| 273 | + doubleClickZoom: false, | |
| 274 | + boxZoom: false, | |
| 275 | + tap: false | |
| 276 | + }).setView(coords, 15); | |
| 277 | + | |
| 278 | + // Add a marker at the location if coordinates exist | |
| 279 | + if (latlng != null) { | |
| 280 | + L.marker(coords).addTo(map); | |
| 281 | + } | |
| 282 | + | |
| 283 | + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
| 284 | + maxZoom: 19, | |
| 285 | + attribution: '© <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>' | |
| 286 | + }).addTo(map); | |
| 287 | + } | |
| 288 | +} | |
| 289 | + | |
| 115 | 290 | let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks}) |
| 116 | 291 | |
| 117 | 292 | // Show progress bar on live navigation and form submits |
modified
apps/room_sanctum/lib/room_sanctum/configuration/vision.ex
+27
−2
@@ -17,10 +17,22 @@ defmodule RoomSanctum.Configuration.Vision do
| 17 | 17 | def changeset(vision, attrs) do |
| 18 | 18 | vision |
| 19 | 19 | |> cast(attrs, [:name, :user_id, :query_ids, :public]) |
| 20 | − |> cast_embed(:queries, with: &RoomSanctum.Configuration.Vision.Schema.changeset/2) | |
| 20 | + |> cast_embed(:queries, with: &RoomSanctum.Configuration.Vision.Schema.changeset/2, sort_param: :queries_sort, drop_param: :queries_drop) | |
| 21 | 21 | |> foreign_key_constraint(:user_id) |
| 22 | 22 | |> validate_required([:name, :user_id]) |
| 23 | + |> validate_queries_have_valid_ids() | |
| 23 | 24 | end |
| 25 | + | |
| 26 | + defp validate_queries_have_valid_ids(changeset) do | |
| 27 | + case get_field(changeset, :query_ids) do | |
| 28 | + ids when is_list(ids) and length(ids) > 0 -> | |
| 29 | + # Validate that all query IDs exist and belong to the user | |
| 30 | + changeset | |
| 31 | + _ -> | |
| 32 | + changeset | |
| 33 | + end | |
| 34 | + end | |
| 35 | + | |
| 24 | 36 | end |
| 25 | 37 | |
| 26 | 38 | defmodule RoomSanctum.Configuration.Vision.Schema do |
@@ -121,6 +133,19 @@ defmodule RoomSanctum.Configuration.Vision.Schema3Background do
| 121 | 133 | def changeset(source, params) do |
| 122 | 134 | source |
| 123 | 135 | |> cast(params, [:query, :order]) |
| 124 | − |> validate_required([:query, :order]) | |
| 136 | + |> validate_required([:order]) | |
| 137 | + |> validate_query_selection() | |
| 138 | + end | |
| 139 | + | |
| 140 | + defp validate_query_selection(changeset) do | |
| 141 | + case get_field(changeset, :query) do | |
| 142 | + query when is_integer(query) and query > 0 -> changeset | |
| 143 | + query when is_binary(query) and query != "" -> | |
| 144 | + case Integer.parse(query) do | |
| 145 | + {id, ""} when id > 0 -> put_change(changeset, :query, id) | |
| 146 | + _ -> add_error(changeset, :query, "must be a valid query ID") | |
| 147 | + end | |
| 148 | + _ -> changeset # Allow empty for new entries | |
| 149 | + end | |
| 125 | 150 | end |
| 126 | 151 | end |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/form_component.html.heex
+12
−0
@@ -25,6 +25,18 @@
| 25 | 25 | </div> |
| 26 | 26 | <div> |
| 27 | 27 | <div class="w-full"> |
| 28 | + <div class="form-control mb-4"> | |
| 29 | + <label class="block text-sm font-semibold leading-6 text-zinc-800 mb-2">Search Location</label> | |
| 30 | + <div class="relative"> | |
| 31 | + <input | |
| 32 | + type="text" | |
| 33 | + id="location-search" | |
| 34 | + placeholder="Search for addresses, places, or POIs..." | |
| 35 | + class="input input-bordered w-full pr-10" | |
| 36 | + /> | |
| 37 | + <div id="search-results" class="absolute z-10 w-full bg-white border border-gray-300 rounded-md shadow-lg hidden max-h-60 overflow-y-auto"></div> | |
| 38 | + </div> | |
| 39 | + </div> | |
| 28 | 40 | <div class="form-control"> |
| 29 | 41 | <label class="block text-sm font-semibold leading-6 text-zinc-800"> |
| 30 | 42 | <div id="structural-shim" phx-update="ignore"> |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/show.ex
+8
−0
@@ -18,4 +18,12 @@ defmodule RoomSanctumWeb.FociLive.Show do
| 18 | 18 | |
| 19 | 19 | defp page_title(:show), do: "Foci Detail" |
| 20 | 20 | defp page_title(:edit), do: "Modify Foci" |
| 21 | + | |
| 22 | + defp getlatlng(%{:place => nil}) do | |
| 23 | + nil | |
| 24 | + end | |
| 25 | + | |
| 26 | + defp getlatlng(%{:place => place}) do | |
| 27 | + place |> Map.get(:coordinates, {}) |> Tuple.to_list() |> Poison.encode!() | |
| 28 | + end | |
| 21 | 29 | end |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/show.html.heex
+12
−1
@@ -22,7 +22,18 @@
| 22 | 22 | <div class="card card-compact w-3/4 bg-base-100 shadow-xl m-2"> |
| 23 | 23 | <div class="card-body"> |
| 24 | 24 | <h3 class="card-title">Place</h3> |
| 25 | − <pre>@foci.place</pre> | |
| 25 | + <div class="mt-4"> | |
| 26 | + <link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css" | |
| 27 | + integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ==" | |
| 28 | + crossorigin=""/> | |
| 29 | + <script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js" | |
| 30 | + integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ==" | |
| 31 | + crossorigin=""></script> | |
| 32 | + | |
| 33 | + <div id="show-map-container" phx-update="ignore"> | |
| 34 | + <div id="show-map" style="height: 300px; width: 100%" phx-hook="mkShowMap" data-latlng={@foci |> getlatlng} class="rounded-sm"></div> | |
| 35 | + </div> | |
| 36 | + </div> | |
| 26 | 37 | </div> |
| 27 | 38 | </div> |
| 28 | 39 | </div> |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/form_component.ex
+243
−158
@@ -1,132 +1,191 @@
| 1 | 1 | defmodule RoomSanctumWeb.VisionLive.FormComponent do |
| 2 | 2 | use RoomSanctumWeb, :live_component |
| 3 | + import PolymorphicEmbed.HTML.Helpers | |
| 3 | 4 | |
| 4 | 5 | alias RoomSanctum.Configuration |
| 6 | + alias RoomSanctum.Configuration.Vision | |
| 5 | 7 | |
| 6 | − defp inj_uid(params, socket) do | |
| 7 | − params | |
| 8 | − # |> IO.inspect | |
| 9 | − |> Map.put("user_id", socket.assigns.current_user.id) | |
| 10 | − |> Map.put( | |
| 11 | − "query_ids", | |
| 12 | − params |> Map.get("queries", %{}) |> Enum.map(fn {k, v} -> v["data"]["query"] end) | |
| 13 | − ) | |
| 14 | − end | |
| 15 | − | |
| 16 | − defp inj_types(params) do | |
| 17 | − params | |
| 18 | − |> Map.put( | |
| 19 | − "queries", | |
| 20 | − params | |
| 21 | − |> Map.get("queries", %{}) | |
| 22 | − |> Enum.map(fn {k, v} -> {k, v |> Kernel.put_in(["data", "__type__"], v["type"])} end) | |
| 23 | − |> Enum.into(%{}) | |
| 24 | − ) | |
| 8 | + @impl true | |
| 9 | + def render(assigns) do | |
| 10 | + ~H""" | |
| 11 | + <div> | |
| 12 | + <.header> | |
| 13 | + <%= @title %> | |
| 14 | + <:subtitle>Configure your vision with custom queries</:subtitle> | |
| 15 | + </.header> | |
| 16 | + | |
| 17 | + <.simple_form | |
| 18 | + for={@form} | |
| 19 | + id="vision-form" | |
| 20 | + phx-target={@myself} | |
| 21 | + phx-change="validate" | |
| 22 | + phx-submit="save" | |
| 23 | + > | |
| 24 | + <.input field={@form[:name]} type="text" label="Name" /> | |
| 25 | + | |
| 26 | + <.input field={@form[:public]} type="checkbox" label="Make this vision public" /> | |
| 27 | + | |
| 28 | + <div class="mt-6"> | |
| 29 | + <h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">Queries</h3> | |
| 30 | + | |
| 31 | + <div id="queries" class="space-y-4"> | |
| 32 | + <.inputs_for :let={query_form} field={@form[:queries]}> | |
| 33 | + <div class="border rounded-lg p-4 relative"> | |
| 34 | + <input type="hidden" name="vision[queries_sort][]" value={query_form.index} /> | |
| 35 | + | |
| 36 | + <div class="absolute top-2 right-2"> | |
| 37 | + <.button | |
| 38 | + type="button" | |
| 39 | + phx-click="remove_query" | |
| 40 | + phx-value-index={query_form.index} | |
| 41 | + phx-target={@myself} | |
| 42 | + class="text-red-600 hover:text-red-800" | |
| 43 | + > | |
| 44 | + <.icon name="hero-trash" class="h-4 w-4" /> | |
| 45 | + </.button> | |
| 46 | + </div> | |
| 47 | + | |
| 48 | + <.input | |
| 49 | + field={query_form[:type]} | |
| 50 | + type="select" | |
| 51 | + label="Query Type" | |
| 52 | + options={[ | |
| 53 | + {"Alerts", :alerts}, | |
| 54 | + {"Time-based", :time}, | |
| 55 | + {"Pinned", :pinned}, | |
| 56 | + {"Background", :background} | |
| 57 | + ]} | |
| 58 | + /> | |
| 59 | + | |
| 60 | + <%= render_query_fields(query_form) %> | |
| 61 | + </div> | |
| 62 | + </.inputs_for> | |
| 63 | + </div> | |
| 64 | + | |
| 65 | + <div class="mt-4"> | |
| 66 | + <.button type="button" phx-click="add_query" phx-target={@myself}> | |
| 67 | + <.icon name="hero-plus" class="h-4 w-4 mr-1" /> Add Query | |
| 68 | + </.button> | |
| 69 | + </div> | |
| 70 | + </div> | |
| 71 | + | |
| 72 | + <:actions> | |
| 73 | + <.button phx-disable-with="Saving...">Save Vision</.button> | |
| 74 | + </:actions> | |
| 75 | + </.simple_form> | |
| 76 | + </div> | |
| 77 | + """ | |
| 25 | 78 | end |
| 26 | 79 | |
| 27 | 80 | @impl true |
| 28 | 81 | def update(%{vision: vision} = assigns, socket) do |
| 29 | − # Ensure vision has an empty queries list if it's nil or empty | |
| 30 | − vision = ensure_queries_initialized(vision) | |
| 31 | 82 | changeset = Configuration.change_vision(vision) |
| 32 | 83 | |
| 33 | − { | |
| 34 | − :ok, | |
| 84 | + {:ok, | |
| 35 | 85 | socket |
| 36 | 86 | |> assign(assigns) |
| 37 | − |> assign_form(changeset) | |
| 38 | − |> assign(:cfg_queries, list_cfg_queries(assigns.current_user.id)) | |
| 39 | − |> assign( | |
| 40 | − :cfg_queries_sel, | |
| 41 | − list_cfg_queries(assigns.current_user.id) | |
| 42 | − |> Enum.map(fn x -> {"#{x.name} (#{x.source.type})", x.id} end) | |
| 43 | − |> Enum.into(%{}) | |
| 44 | − ) | |
| 45 | − } | |
| 46 | − end | |
| 47 | − | |
| 48 | − defp ensure_queries_initialized(%{queries: queries} = vision) when is_list(queries), do: vision | |
| 49 | − defp ensure_queries_initialized(vision) do | |
| 50 | − %{vision | queries: []} | |
| 87 | + |> assign_form(changeset)} | |
| 51 | 88 | end |
| 52 | 89 | |
| 53 | 90 | @impl true |
| 54 | 91 | def handle_event("validate", %{"vision" => vision_params}, socket) do |
| 55 | − IO.puts("cvl-valid") | |
| 56 | − vision_params = inj_uid(vision_params, socket) | |
| 57 | − | |
| 58 | 92 | changeset = |
| 59 | 93 | socket.assigns.vision |
| 60 | 94 | |> Configuration.change_vision(vision_params) |
| 61 | 95 | |> Map.put(:action, :validate) |
| 62 | 96 | |
| 63 | − # |> IO.inspect() | |
| 64 | − | |
| 65 | 97 | {:noreply, assign_form(socket, changeset)} |
| 66 | 98 | end |
| 67 | 99 | |
| 68 | 100 | def handle_event("save", %{"vision" => vision_params}, socket) do |
| 69 | − IO.puts("cvl-save") | |
| 70 | − vision_params = inj_uid(vision_params, socket) |> inj_types | |
| 71 | 101 | save_vision(socket, socket.assigns.action, vision_params) |
| 72 | 102 | end |
| 73 | 103 | |
| 74 | − def handle_event("add-entry", _data, socket) do | |
| 75 | − # IO.inspect(socket.assigns.form) | |
| 76 | − f = socket.assigns.form | |
| 77 | − | |
| 78 | − existing_as_simple = | |
| 79 | − case f.params do | |
| 80 | − map when map == %{} -> | |
| 81 | − f.data.queries | |
| 82 | − |> Poison.encode!() | |
| 83 | − |> Poison.decode!() | |
| 84 | − |> Enum.with_index() | |
| 85 | − |> Enum.map(fn {k, v} -> {"#{v}", k} end) | |
| 86 | − |> Map.new() | |
| 104 | + def handle_event("query_type_changed", %{"index" => index, "value" => new_type}, socket) do | |
| 105 | + existing_queries = Map.get(socket.assigns.form.params, "queries", %{}) | |
| 87 | 106 | |
| 88 | − # |> Enum.map(fn {k,v} -> v end) | |
| 89 | − otherwise -> | |
| 90 | − f.params |> Map.get("queries") | |
| 107 | + updated_query = | |
| 108 | + case Map.get(existing_queries, index) do | |
| 109 | + nil -> %{"type" => new_type} | |
| 110 | + query -> | |
| 111 | + order = get_in(query, ["data", "order"]) || 1 | |
| 112 | + Map.put(query, "data", build_data_for_type(new_type, order)) | |
| 91 | 113 | end |
| 92 | 114 | |
| 93 | − # IO.inspect(existing_as_simple) | |
| 94 | − combined = | |
| 95 | − case existing_as_simple do | |
| 96 | − list when list == [] -> | |
| 97 | − %{ | |
| 98 | − "0" => %{ | |
| 99 | − "data" => %{"__type__" => "pinned", "query" => 0}, | |
| 100 | − "id" => nil, | |
| 101 | − "type" => "pinned", | |
| 102 | − "order" => 0 | |
| 103 | − } | |
| 104 | − } | |
| 105 | − | |
| 106 | − _otherwise -> | |
| 107 | − existing_as_simple | |
| 108 | − |> Map.merge(%{ | |
| 109 | − "#{existing_as_simple |> Map.keys() |> length}" => %{ | |
| 110 | − "data" => %{"__type__" => "pinned", "query" => 0}, | |
| 111 | − "id" => nil, | |
| 112 | − "type" => "pinned", | |
| 113 | − "order" => 0 | |
| 114 | − } | |
| 115 | − }) | |
| 116 | − end | |
| 115 | + updated_params = | |
| 116 | + socket.assigns.form.params | |
| 117 | + |> Map.put("queries", Map.put(existing_queries, index, updated_query)) | |
| 117 | 118 | |
| 118 | 119 | changeset = |
| 119 | 120 | socket.assigns.vision |
| 120 | − |> Configuration.change_vision(%{queries: combined}) | |
| 121 | + |> Configuration.change_vision(updated_params) | |
| 121 | 122 | |> Map.put(:action, :validate) |
| 122 | 123 | |
| 123 | − # |> IO.inspect() | |
| 124 | + {:noreply, assign_form(socket, changeset)} | |
| 125 | + end | |
| 124 | 126 | |
| 125 | − { | |
| 126 | − :noreply, | |
| 127 | − socket | |
| 128 | − |> assign_form(changeset) | |
| 127 | + defp build_data_for_type(type, order) do | |
| 128 | + base_data = %{"__type__" => type, "order" => order} | |
| 129 | + | |
| 130 | + case type do | |
| 131 | + "time" -> | |
| 132 | + Map.merge(base_data, %{ | |
| 133 | + "query" => nil, | |
| 134 | + "weekdays" => [], | |
| 135 | + "time_start" => nil, | |
| 136 | + "time_end" => nil | |
| 137 | + }) | |
| 138 | + | |
| 139 | + "background" -> | |
| 140 | + # Background query is optional, so we don't include it | |
| 141 | + base_data | |
| 142 | + | |
| 143 | + _ -> | |
| 144 | + # alerts and pinned require query | |
| 145 | + Map.put(base_data, "query", nil) | |
| 146 | + end | |
| 147 | + end | |
| 148 | + | |
| 149 | + def handle_event("add_query", _params, socket) do | |
| 150 | + existing_queries = Map.get(socket.assigns.form.params, "queries", %{}) | |
| 151 | + next_index = map_size(existing_queries) | |
| 152 | + | |
| 153 | + new_query = %{ | |
| 154 | + "type" => "pinned", | |
| 155 | + "data" => %{ | |
| 156 | + "__type__" => "pinned", | |
| 157 | + "order" => next_index + 1, | |
| 158 | + "query" => nil | |
| 159 | + } | |
| 129 | 160 | } |
| 161 | + | |
| 162 | + updated_params = | |
| 163 | + socket.assigns.form.params | |
| 164 | + |> Map.put("queries", Map.put(existing_queries, to_string(next_index), new_query)) | |
| 165 | + | |
| 166 | + changeset = | |
| 167 | + socket.assigns.vision | |
| 168 | + |> Configuration.change_vision(updated_params) | |
| 169 | + |> Map.put(:action, :validate) | |
| 170 | + | |
| 171 | + {:noreply, assign_form(socket, changeset)} | |
| 172 | + end | |
| 173 | + | |
| 174 | + def handle_event("remove_query", %{"index" => index}, socket) do | |
| 175 | + existing_queries = Map.get(socket.assigns.form.params, "queries", %{}) | |
| 176 | + updated_queries = Map.delete(existing_queries, index) | |
| 177 | + | |
| 178 | + updated_params = | |
| 179 | + socket.assigns.form.params | |
| 180 | + |> Map.put("queries", updated_queries) | |
| 181 | + |> Map.put("queries_drop", [index]) | |
| 182 | + | |
| 183 | + changeset = | |
| 184 | + socket.assigns.vision | |
| 185 | + |> Configuration.change_vision(updated_params) | |
| 186 | + |> Map.put(:action, :validate) | |
| 187 | + | |
| 188 | + {:noreply, assign_form(socket, changeset)} | |
| 130 | 189 | end |
| 131 | 190 | |
| 132 | 191 | defp save_vision(socket, :edit, vision_params) do |
@@ -134,12 +193,10 @@ defmodule RoomSanctumWeb.VisionLive.FormComponent do
| 134 | 193 | {:ok, vision} -> |
| 135 | 194 | notify_parent({:saved, vision}) |
| 136 | 195 | |
| 137 | − { | |
| 138 | − :noreply, | |
| 196 | + {:noreply, | |
| 139 | 197 | socket |
| 140 | 198 | |> put_flash(:info, "Vision updated successfully") |
| 141 | − |> push_redirect(to: socket.assigns.patch) | |
| 142 | − } | |
| 199 | + |> push_patch(to: socket.assigns.patch)} | |
| 143 | 200 | |
| 144 | 201 | {:error, %Ecto.Changeset{} = changeset} -> |
| 145 | 202 | {:noreply, assign_form(socket, changeset)} |
@@ -147,16 +204,16 @@ defmodule RoomSanctumWeb.VisionLive.FormComponent do
| 147 | 204 | end |
| 148 | 205 | |
| 149 | 206 | defp save_vision(socket, :new, vision_params) do |
| 207 | + vision_params = Map.put(vision_params, "user_id", socket.assigns.current_user.id) | |
| 208 | + | |
| 150 | 209 | case Configuration.create_vision(vision_params) do |
| 151 | 210 | {:ok, vision} -> |
| 152 | 211 | notify_parent({:saved, vision}) |
| 153 | 212 | |
| 154 | − { | |
| 155 | − :noreply, | |
| 213 | + {:noreply, | |
| 156 | 214 | socket |
| 157 | 215 | |> put_flash(:info, "Vision created successfully") |
| 158 | − |> push_redirect(to: socket.assigns.patch) | |
| 159 | − } | |
| 216 | + |> push_patch(to: socket.assigns.patch)} | |
| 160 | 217 | |
| 161 | 218 | {:error, %Ecto.Changeset{} = changeset} -> |
| 162 | 219 | {:noreply, assign_form(socket, changeset)} |
@@ -169,76 +226,104 @@ defmodule RoomSanctumWeb.VisionLive.FormComponent do
| 169 | 226 | |
| 170 | 227 | defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) |
| 171 | 228 | |
| 172 | − defp list_cfg_queries(uid) do | |
| 173 | − Configuration.list_cfg_queries({:user, uid}) | |
| 174 | − end | |
| 175 | − | |
| 176 | − defp safe_inputs_with_index(form, field) do | |
| 177 | − case inputs_for(form, field) do | |
| 178 | − {:safe, []} -> [] | |
| 179 | − inputs when is_list(inputs) -> Enum.with_index(inputs) | |
| 180 | − _ -> [] | |
| 229 | + defp render_query_fields(query_form) do | |
| 230 | + case Ecto.Changeset.get_field(query_form.source, :type) do | |
| 231 | + :alerts -> render_alerts_fields(query_form) | |
| 232 | + :time -> render_time_fields(query_form) | |
| 233 | + :pinned -> render_pinned_fields(query_form) | |
| 234 | + :background -> render_background_fields(query_form) | |
| 235 | + _ -> render_empty_state() | |
| 181 | 236 | end |
| 182 | 237 | end |
| 183 | 238 | |
| 184 | − defp etuple(:U) do | |
| 185 | − {"Sunday", "U"} | |
| 186 | − end | |
| 187 | − | |
| 188 | − defp etuple(:M) do | |
| 189 | − {"Monday", "M"} | |
| 239 | + defp render_alerts_fields(query_form) do | |
| 240 | + assigns = %{query_form: query_form} | |
| 241 | + | |
| 242 | + ~H""" | |
| 243 | + <%= for data_form <- polymorphic_embed_inputs_for(@query_form, :data) do %> | |
| 244 | + <%= hidden_inputs_for(data_form) %> | |
| 245 | + <div class="grid grid-cols-2 gap-4 mt-4"> | |
| 246 | + <.input field={data_form[:query]} type="number" label="Query ID" /> | |
| 247 | + <.input field={data_form[:order]} type="number" label="Order" /> | |
| 248 | + </div> | |
| 249 | + <% end %> | |
| 250 | + """ | |
| 190 | 251 | end |
| 191 | 252 | |
| 192 | − defp etuple(:T) do | |
| 193 | − {"Tuesday", "T"} | |
| 253 | + defp render_time_fields(query_form) do | |
| 254 | + assigns = %{query_form: query_form} | |
| 255 | + | |
| 256 | + ~H""" | |
| 257 | + <%= for data_form <- polymorphic_embed_inputs_for(@query_form, :data) do %> | |
| 258 | + <%= hidden_inputs_for(data_form) %> | |
| 259 | + <div class="space-y-4 mt-4"> | |
| 260 | + <div class="grid grid-cols-2 gap-4"> | |
| 261 | + <.input field={data_form[:query]} type="number" label="Query ID" /> | |
| 262 | + <.input field={data_form[:order]} type="number" label="Order" /> | |
| 263 | + </div> | |
| 264 | + | |
| 265 | + <div> | |
| 266 | + <label class="block text-sm font-medium text-gray-700 mb-2">Active Days</label> | |
| 267 | + <div class="flex gap-2"> | |
| 268 | + <%= for {label, value} <- [{"Sun", :U}, {"Mon", :M}, {"Tue", :T}, {"Wed", :W}, {"Thu", :R}, {"Fri", :F}, {"Sat", :S}] do %> | |
| 269 | + <label class="inline-flex items-center"> | |
| 270 | + <input | |
| 271 | + type="checkbox" | |
| 272 | + name={"vision[queries][#{@query_form.index}][data][weekdays][]"} | |
| 273 | + value={value} | |
| 274 | + checked={value in (Ecto.Changeset.get_field(data_form.source, :weekdays) || [])} | |
| 275 | + class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" | |
| 276 | + /> | |
| 277 | + <span class="ml-1 text-sm"><%= label %></span> | |
| 278 | + </label> | |
| 279 | + <% end %> | |
| 280 | + </div> | |
| 281 | + </div> | |
| 282 | + | |
| 283 | + <div class="grid grid-cols-2 gap-4"> | |
| 284 | + <.input field={data_form[:time_start]} type="time" label="Start Time" /> | |
| 285 | + <.input field={data_form[:time_end]} type="time" label="End Time" /> | |
| 286 | + </div> | |
| 287 | + </div> | |
| 288 | + <% end %> | |
| 289 | + """ | |
| 194 | 290 | end |
| 195 | 291 | |
| 196 | − defp etuple(:W) do | |
| 197 | − {"Wednesday", "W"} | |
| 292 | + defp render_pinned_fields(query_form) do | |
| 293 | + assigns = %{query_form: query_form} | |
| 294 | + | |
| 295 | + ~H""" | |
| 296 | + <%= for data_form <- polymorphic_embed_inputs_for(@query_form, :data) do %> | |
| 297 | + <%= hidden_inputs_for(data_form) %> | |
| 298 | + <div class="grid grid-cols-2 gap-4 mt-4"> | |
| 299 | + <.input field={data_form[:query]} type="number" label="Query ID" /> | |
| 300 | + <.input field={data_form[:order]} type="number" label="Order" /> | |
| 301 | + </div> | |
| 302 | + <% end %> | |
| 303 | + """ | |
| 198 | 304 | end |
| 199 | 305 | |
| 200 | − defp etuple(:R) do | |
| 201 | − {"Thursday", "R"} | |
| 306 | + defp render_background_fields(query_form) do | |
| 307 | + assigns = %{query_form: query_form} | |
| 308 | + | |
| 309 | + ~H""" | |
| 310 | + <%= for data_form <- polymorphic_embed_inputs_for(@query_form, :data) do %> | |
| 311 | + <%= hidden_inputs_for(data_form) %> | |
| 312 | + <div class="grid grid-cols-2 gap-4 mt-4"> | |
| 313 | + <.input field={data_form[:query]} type="number" label="Query ID (optional)" /> | |
| 314 | + <.input field={data_form[:order]} type="number" label="Order" /> | |
| 315 | + </div> | |
| 316 | + <% end %> | |
| 317 | + """ | |
| 202 | 318 | end |
| 203 | 319 | |
| 204 | − defp etuple(:F) do | |
| 205 | − {"Friday", "F"} | |
| 206 | − end | |
| 207 | − | |
| 208 | − defp etuple(:S) do | |
| 209 | − {"Saturday", "S"} | |
| 210 | − end | |
| 211 | − | |
| 212 | − defp gfv(form, fq, ctr) do | |
| 213 | − # IO.puts("GFV") | |
| 214 | − # IO.inspect(changeset) | |
| 215 | − # IO.inspect(fq) | |
| 216 | − # | |
| 217 | − # IO.inspect( | |
| 218 | − # fq.data | |
| 219 | − # |> Map.get(:type) | |
| 220 | − # ) | |
| 221 | − # IO.inspect( | |
| 222 | − # fq.source.data | |
| 223 | − # |> Map.get(:type) | |
| 224 | − # ) | |
| 225 | − | |
| 226 | − q = | |
| 227 | − form.params |> Map.get("queries", %{}) |> Map.get("#{ctr}", %{}) |> Map.get("type") || | |
| 228 | − form.data | |
| 229 | − |> Map.get(:queries, []) | |
| 230 | − |> Enum.at(ctr, %{}) | |
| 231 | − |> Map.get(:changes, %{}) | |
| 232 | − |> Map.get(:type) || | |
| 233 | − fq.data | |
| 234 | − |> Map.get(:source, %{}) | |
| 235 | − |> Map.get(:changes, %{}) | |
| 236 | − |> Map.get(:type) || | |
| 237 | − fq.source | |
| 238 | − |> Map.get(:data, %{}) | |
| 239 | − |> Map.get(:type) | |
| 320 | + defp render_empty_state do | |
| 321 | + assigns = %{} | |
| 240 | 322 | |
| 241 | − # |> IO.inspect | |
| 242 | − q | |
| 323 | + ~H""" | |
| 324 | + <div class="mt-4 text-sm text-gray-500"> | |
| 325 | + Select a query type to configure | |
| 326 | + </div> | |
| 327 | + """ | |
| 243 | 328 | end |
| 244 | −end | |
| 329 | +end |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/form_component.html.heex
+116
−61
@@ -1,77 +1,132 @@
| 1 | 1 | <div> |
| 2 | − <h2 class="text-2xl font-bold text-accent"><%= @title %></h2> | |
| 2 | + <.header> | |
| 3 | + <%= @title %> | |
| 4 | + <:subtitle>Configure your vision with custom queries</:subtitle> | |
| 5 | + </.header> | |
| 3 | 6 | |
| 4 | − <.simple_form | |
| 7 | + <.simple_form | |
| 5 | 8 | for={@form} |
| 6 | 9 | id="vision-form" |
| 7 | 10 | phx-target={@myself} |
| 8 | 11 | phx-change="validate" |
| 9 | − phx-submit="save"> | |
| 12 | + phx-submit="save" | |
| 13 | + > | |
| 14 | + <.input field={@form[:name]} type="text" label="Name" /> | |
| 10 | 15 | |
| 11 | − <div class="flex"> | |
| 12 | − <div class="w-full"> | |
| 13 | − <div class="form-control"> | |
| 14 | − <.input field={@form[:name]} type="text" label="Name"/> | |
| 15 | − </div> | |
| 16 | − </div> | |
| 17 | − </div> | |
| 18 | − <%= for {fq, ctr} <- safe_inputs_with_index(@form, :queries) do %> | |
| 19 | − <%= hidden_inputs_for(fq) %> | |
| 20 | − <div class="flex"> | |
| 21 | − <div class="flex-none w-16 font-bold text-7xl py-6"> | |
| 22 | − <%= ctr %> | |
| 23 | − </div> | |
| 24 | − <div class="flex-none w-32 p-1"> | |
| 25 | − <div class="form-control"> | |
| 26 | − <.input field={fq[:type]} type="select" label="Type" options={ Ecto.Enum.values(RoomSanctum.Configuration.Vision, :type)}/> | |
| 27 | − </div> | |
| 16 | + <.input field={@form[:public]} type="checkbox" label="Make this vision public (shown on homepage)" /> | |
| 17 | + | |
| 18 | + <div class="mt-6"> | |
| 19 | + <h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">Queries</h3> | |
| 20 | + | |
| 21 | + <div id="queries" class="space-y-4"> | |
| 22 | + <.inputs_for :let={query_form} field={@form[:queries]}> | |
| 23 | + <div class="border rounded-lg p-4 relative"> | |
| 24 | + <input type="hidden" name="vision[queries_sort][]" value={query_form.index} /> | |
| 25 | + | |
| 26 | + <div class="absolute top-2 right-2"> | |
| 27 | + <.button | |
| 28 | + type="button" | |
| 29 | + phx-click="remove_query" | |
| 30 | + phx-value-index={query_form.index} | |
| 31 | + phx-target={@myself} | |
| 32 | + class="text-red-600 hover:text-red-800" | |
| 33 | + > | |
| 34 | + <.icon name="hero-trash" class="h-4 w-4" /> | |
| 35 | + </.button> | |
| 28 | 36 | </div> |
| 29 | 37 | |
| 30 | − <%= for fd <- polymorphic_embed_inputs_for fq, :data do %> | |
| 31 | − <%= hidden_inputs_for(fd) %> | |
| 32 | − <div class="flex flex-col"> | |
| 33 | − <div class="flex"> | |
| 34 | − <div class="flex-none w-24 p-1"> | |
| 35 | − <div class="form-control"> | |
| 36 | − <.input field={fd[:order]} type="number" label="Order" /> | |
| 38 | + <.input | |
| 39 | + field={query_form[:type]} | |
| 40 | + type="select" | |
| 41 | + label="Query Type" | |
| 42 | + options={[ | |
| 43 | + {"Alerts", :alerts}, | |
| 44 | + {"Time-based", :time}, | |
| 45 | + {"Pinned", :pinned}, | |
| 46 | + {"Background", :background} | |
| 47 | + ]} | |
| 48 | + /> | |
| 49 | + | |
| 50 | + <%= case Ecto.Changeset.get_field(query_form.source, :type) do %> | |
| 51 | + <% :alerts -> %> | |
| 52 | + <%= for data_form <- polymorphic_embed_inputs_for(query_form, :data) do %> | |
| 53 | + <%= hidden_inputs_for(data_form) %> | |
| 54 | + <div class="grid grid-cols-2 gap-4 mt-4"> | |
| 55 | + <.input field={data_form[:query]} type="number" label="Query ID" /> | |
| 56 | + <.input field={data_form[:order]} type="number" label="Order" /> | |
| 57 | + </div> | |
| 58 | + <% end %> | |
| 59 | + | |
| 60 | + <% :time -> %> | |
| 61 | + <%= for data_form <- polymorphic_embed_inputs_for(query_form, :data) do %> | |
| 62 | + <%= hidden_inputs_for(data_form) %> | |
| 63 | + <div class="space-y-4 mt-4"> | |
| 64 | + <div class="grid grid-cols-2 gap-4"> | |
| 65 | + <.input field={data_form[:query]} type="number" label="Query ID" /> | |
| 66 | + <.input field={data_form[:order]} type="number" label="Order" /> | |
| 37 | 67 | </div> |
| 38 | − </div> | |
| 39 | − <div class="flex-1 w-full p-1"> | |
| 40 | − <div class="form-control"> | |
| 41 | − <.input field={fd[:query]} type="select" label="Type" options={@cfg_queries_sel}/> | |
| 68 | + | |
| 69 | + <div> | |
| 70 | + <label class="block text-sm font-medium text-gray-700 mb-2">Active Days</label> | |
| 71 | + <div class="flex gap-2"> | |
| 72 | + <%= for {label, value} <- [{"Sun", :U}, {"Mon", :M}, {"Tue", :T}, {"Wed", :W}, {"Thu", :R}, {"Fri", :F}, {"Sat", :S}] do %> | |
| 73 | + <label class="inline-flex items-center"> | |
| 74 | + <input | |
| 75 | + type="checkbox" | |
| 76 | + name={"vision[queries][#{query_form.index}][data][weekdays][]"} | |
| 77 | + value={value} | |
| 78 | + checked={value in (Ecto.Changeset.get_field(data_form.source, :weekdays) || [])} | |
| 79 | + class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" | |
| 80 | + /> | |
| 81 | + <span class="ml-1 text-sm"><%= label %></span> | |
| 82 | + </label> | |
| 83 | + <% end %> | |
| 84 | + </div> | |
| 42 | 85 | </div> |
| 43 | − </div> | |
| 44 | − </div> | |
| 45 | − <div class="flex-1 w-full p-1"> | |
| 46 | − <%= case gfv(@form, fq, ctr) do %> | |
| 47 | − <% val when val in [:time, "time"] -> %> | |
| 48 | − <div class="flex"> | |
| 49 | − <div class="form-control"> | |
| 50 | − <.input field={fd[:weekdays]} type="select" multiple="true" label="Weekday" options={Enum.map(Ecto.Enum.values(RoomSanctum.Configuration.Vision.Schema1Time, :weekdays), fn c -> etuple(c) end)}/> | |
| 51 | − </div> | |
| 52 | − <div class="px-4 | |
| 53 | −"> | |
| 54 | − <label class="block text-md font-bold leading-6 pt-4 text-primary-content">Times</label> | |
| 55 | − <div class="time-inputs"> | |
| 56 | − <.input field={fd[:time_start]} type="time" label="Start")} /> | |
| 57 | − </div> | |
| 58 | − <div class="time-inputs"> | |
| 59 | − <.input field={fd[:time_end]} type="time" label="End")} /> | |
| 60 | − </div> | |
| 61 | − </div> | |
| 86 | + | |
| 87 | + <div class="grid grid-cols-2 gap-4"> | |
| 88 | + <.input field={data_form[:time_start]} type="time" label="Start Time" /> | |
| 89 | + <.input field={data_form[:time_end]} type="time" label="End Time" /> | |
| 62 | 90 | </div> |
| 63 | − <% _ -> %> | |
| 91 | + </div> | |
| 92 | + <% end %> | |
| 93 | + | |
| 94 | + <% :pinned -> %> | |
| 95 | + <%= for data_form <- polymorphic_embed_inputs_for(query_form, :data) do %> | |
| 96 | + <%= hidden_inputs_for(data_form) %> | |
| 97 | + <div class="grid grid-cols-2 gap-4 mt-4"> | |
| 98 | + <.input field={data_form[:query]} type="number" label="Query ID" /> | |
| 99 | + <.input field={data_form[:order]} type="number" label="Order" /> | |
| 100 | + </div> | |
| 64 | 101 | <% end %> |
| 102 | + | |
| 103 | + <% :background -> %> | |
| 104 | + <%= for data_form <- polymorphic_embed_inputs_for(query_form, :data) do %> | |
| 105 | + <%= hidden_inputs_for(data_form) %> | |
| 106 | + <div class="grid grid-cols-2 gap-4 mt-4"> | |
| 107 | + <.input field={data_form[:query]} type="number" label="Query ID (optional)" /> | |
| 108 | + <.input field={data_form[:order]} type="number" label="Order" /> | |
| 109 | + </div> | |
| 110 | + <% end %> | |
| 111 | + | |
| 112 | + <% _ -> %> | |
| 113 | + <div class="mt-4 text-sm text-gray-500"> | |
| 114 | + Select a query type to configure | |
| 65 | 115 | </div> |
| 66 | − </div> | |
| 67 | − <% end %> | |
| 68 | − </div> | |
| 69 | − <% end %> | |
| 116 | + <% end %> | |
| 117 | + </div> | |
| 118 | + </.inputs_for> | |
| 119 | + </div> | |
| 70 | 120 | |
| 71 | − <br /> | |
| 72 | − <div class="flex m-2"> | |
| 73 | − <%= submit "Save", phx_disable_with: "Saving...", class: "btn btn-secondary mx-2" %> | |
| 74 | − <button href="#" phx-click="add-entry" phx-target={@myself} class="btn btn-secondary mx-2">Add Entry</button> | |
| 121 | + <div class="mt-4"> | |
| 122 | + <.button type="button" phx-click="add_query" phx-target={@myself}> | |
| 123 | + <.icon name="hero-plus" class="h-4 w-4 mr-1" /> Add Query | |
| 124 | + </.button> | |
| 125 | + </div> | |
| 75 | 126 | </div> |
| 76 | −</.simple_form> | |
| 77 | −</div> | |
| 127 | + | |
| 128 | + <:actions> | |
| 129 | + <.button phx-disable-with="Saving...">Save Vision</.button> | |
| 130 | + </:actions> | |
| 131 | + </.simple_form> | |
| 132 | +</div> |
Parents: 6138b19