diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/REUSE.toml b/REUSE.toml index 36324a99..310c3882 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -5,7 +5,7 @@ version = 1 [[annotations]] -path = ["REUSE.toml", "priv/static/assets/app*"] +path = ["REUSE.toml", ".gitattributes", "priv/static/assets/app*"] SPDX-FileCopyrightText = "2020 Zach Daniel" SPDX-License-Identifier = "MIT" @@ -14,6 +14,16 @@ path = "assets/vendor/topbar.js" SPDX-FileCopyrightText = "2021 Buu Nguyen" SPDX-License-Identifier = "MIT" +[[annotations]] +path = "assets/vendor/sortable.js" +SPDX-FileCopyrightText = "SortableJS contributors " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = ["assets/package.json", "assets/package-lock.json"] +SPDX-FileCopyrightText = "2020 ash_admin contributors " +SPDX-License-Identifier = "MIT" + [[annotations]] path = ["assets/vendor/heroicons/*", "assets/vendor/heroicons/**/*"] SPDX-FileCopyrightText = "2020 Refactoring UI Inc" diff --git a/assets/css/app.css b/assets/css/app.css index 04902cef..79edc74a 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -21,6 +21,18 @@ main.admin-main::after { height: 4rem; } +/* Sortable.js drag-and-drop for array fields */ + +/* placeholder element is slightly transparent */ +.sortable-ghost { + opacity: 0.4; +} + +/* draggable element is styled to indicate it's being dragged */ +.sortable-drag { + cursor: grabbing; +} + /* CodeMirror 6 editor styling */ .cm-editor { min-height: 200px; diff --git a/assets/js/app.js b/assets/js/app.js index 60f25a3a..0950a78f 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -4,6 +4,8 @@ // SPDX-License-Identifier: MIT import topbar from "../vendor/topbar"; +// vendored Sortable.js for drag-and-drop reordering of primitive array fields +import Sortable from "../vendor/sortable"; import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, @@ -345,6 +347,39 @@ Hooks.MaintainAttrs = { }, }; +// Sortable.js drag-and-drop for array fields +Hooks.Sortable = { + // initialize Sortable on the array container when the LiveView hook mounts + mounted() { + this.sortable = new Sortable(this.el, { + animation: 150, + draggable: '[data-sortable="true"]', + handle: '[data-sort-handle="true"]', + ghostClass: "sortable-ghost", + dragClass: "sortable-drag", + forceFallback: true, + // after a drop, send the new row order to the server + onEnd: () => { + const indices = Array.from( + this.el.querySelectorAll('[data-sortable="true"]') + ).map((el) => el.dataset.sortIndex); + if (indices.length < 2) return; + this.pushEventTo(this.el, "update_array_sorting", { + path: this.el.dataset.path, + field: this.el.dataset.field, + indices: indices, + }); + }, + }); + }, + // tear down the Sortable instance when the hook is destroyed + destroyed() { + if (this.sortable) { + this.sortable.destroy(); + } + }, +}; + Hooks.Typeahead = { mounted() { this.aborter = new AbortController(); diff --git a/assets/package-lock.json b/assets/package-lock.json index d4aadfd2..4c431fcc 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -444,7 +444,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -931,7 +933,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -973,6 +977,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -1320,6 +1325,7 @@ "node_modules/tailwindcss": { "version": "3.4.17", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -1481,13 +1487,18 @@ } }, "node_modules/yaml": { - "version": "2.8.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } } } diff --git a/assets/vendor/sortable.js b/assets/vendor/sortable.js new file mode 100644 index 00000000..707ddf0c --- /dev/null +++ b/assets/vendor/sortable.js @@ -0,0 +1,4 @@ +// Vendored Sortable.js for drag-and-drop reordering of primitive array fields in admin forms. +// Source: https://github.com/SortableJS/Sortable (v1.15.6, MIT) +/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return kt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag)),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY + +SPDX-License-Identifier: MIT diff --git a/dev/resources/accounts/resources/calculator.ex b/dev/resources/accounts/resources/calculator.ex index a5b650e3..eb8a38a5 100644 --- a/dev/resources/accounts/resources/calculator.ex +++ b/dev/resources/accounts/resources/calculator.ex @@ -37,5 +37,14 @@ defmodule Demo.Accounts.Calculator do :ok end end + + # demo: primitive array arg for drag-and-drop sorting in GenericAction forms + action :echo_tags, {:array, :string} do + argument :tags, {:array, :string}, allow_nil?: true, public?: true + + run fn input, _ -> + {:ok, List.wrap(input.arguments[:tags])} + end + end end end diff --git a/dev/resources/accounts/resources/user.ex b/dev/resources/accounts/resources/user.ex index 3f3a27df..007f3d8c 100644 --- a/dev/resources/accounts/resources/user.ex +++ b/dev/resources/accounts/resources/user.ex @@ -23,15 +23,14 @@ defmodule Demo.Accounts.User do end show_action :read - read_actions [:me, :read, :by_id, :by_name] + read_actions [:me, :read, :by_id, :by_name, :filter_by_tags] table_columns [:id, :first_name, :last_name, :representative, :admin, :full_name, :api_key, :date_of_birth] table_filterable_columns [:first_name] table_sortable_columns [:first_name, :last_name] - show_calculations [:multi_arguments, :is_super_admin?, :full_name, :nested_embed] + show_calculations [:multi_arguments, :is_super_admin?, :full_name, :nested_embed, :join_tags] end - multitenancy do strategy :attribute attribute :org @@ -67,6 +66,11 @@ defmodule Demo.Accounts.User do filter expr(first_name == ^arg(:first_name) and last_name == ^arg(:last_name)) end + # demo: primitive array arg for drag-and-drop sorting in DataTable query forms + read :filter_by_tags do + argument :tags, {:array, :string}, allow_nil?: true, public?: true + end + create :create do argument :offices, {:array, :map} change manage_relationship(:offices, type: :append) @@ -126,6 +130,11 @@ defmodule Demo.Accounts.User do calculate :nested_embed, :string, expr(tags) do argument :nested_embed, Demo.Accounts.NestedEmbed, allow_nil?: false end + + # demo: primitive array arg for drag-and-drop sorting on Show calculation forms + calculate :join_tags, :string, expr("") do + argument :tags, {:array, :string}, allow_nil?: true + end end attributes do diff --git a/lib/ash_admin/components/resource/data_table.ex b/lib/ash_admin/components/resource/data_table.ex index b49a8f89..55088e5e 100644 --- a/lib/ash_admin/components/resource/data_table.ex +++ b/lib/ash_admin/components/resource/data_table.ex @@ -354,7 +354,11 @@ defmodule AshAdmin.Components.Resource.DataTable do end def handle_event("validate", %{"query" => query}, socket) do - query = AshPhoenix.Form.validate(socket.assigns.query, query) + query = + AshPhoenix.Form.validate( + socket.assigns.query, + AshAdmin.Helpers.sanitize_form_params(query) + ) {:noreply, assign(socket, query: query)} end @@ -363,7 +367,10 @@ defmodule AshAdmin.Components.Resource.DataTable do {:noreply, push_navigate( socket, - to: self_path(socket.assigns.url_path, socket.assigns.params, %{"args" => query_params}) + to: + self_path(socket.assigns.url_path, socket.assigns.params, %{ + "args" => AshAdmin.Helpers.sanitize_form_params(query_params) + }) )} end @@ -434,6 +441,23 @@ defmodule AshAdmin.Components.Resource.DataTable do |> assign(:query, query)} end + def handle_event( + "update_array_sorting", + %{"path" => path, "field" => field, "indices" => indices}, + socket + ) do + query = + AshPhoenix.Form.update_form( + socket.assigns.query, + path, + &sort_array_value(&1, field, indices) + ) + + {:noreply, + socket + |> assign(:query, query)} + end + defp indexed_list(map) when is_map(map) do map |> Map.keys() @@ -502,6 +526,17 @@ defmodule AshAdmin.Components.Resource.DataTable do AshPhoenix.Form.validate(form, new_params) end + defp sort_array_value(form, field, indices) do + new_value = + form + |> AshPhoenix.Form.value(String.to_existing_atom(field)) + |> reorder_by_indices(indices) + + new_params = Map.put(form.params, field, new_value) + + AshPhoenix.Form.validate(form, new_params) + end + # Field rendering - delegate to existing Table component logic defp render_field_value(record, field_name, assigns) do attribute = Ash.Resource.Info.field(assigns.resource, field_name) diff --git a/lib/ash_admin/components/resource/form.ex b/lib/ash_admin/components/resource/form.ex index 68095215..6b7c606c 100644 --- a/lib/ash_admin/components/resource/form.ex +++ b/lib/ash_admin/components/resource/form.ex @@ -1744,6 +1744,9 @@ defmodule AshAdmin.Components.Resource.Form do name = name || form.name <> "[#{attribute.name}]" id = id || form.id <> "_#{attribute.name}" + # normalize array items for rendering and Sortable row metadata + fallback_list = list_value(value || value(value, form, attribute), type) + assigns = assign(assigns, form: form, @@ -1752,40 +1755,58 @@ defmodule AshAdmin.Components.Resource.Form do value: value, name: name, id: id, - union_type: union_type || default_union_type(type, attribute.constraints[:items] || []) + union_type: union_type || default_union_type(type, attribute.constraints[:items] || []), + fallback_list: fallback_list ) ~H"""
+ <%!-- Sortable.js container: hook reads data-path/field and row data-sort-index values --%>
"_sortable_list"} + phx-hook="Sortable" + phx-target={@myself} + data-path={@form.name} + data-field={@attribute.name} > -
- {render_attribute_input( - assigns, - %{@attribute | type: @type, constraints: @attribute.constraints[:items] || []}, - @form, - {:list_value, this_value}, - @name <> "[#{index}]", - @id <> "_#{index}", - @union_type - )} -
- + <%!-- grip handle only; inputs stay editable outside the drag handle --%> + +
+ {render_attribute_input( + assigns, + %{@attribute | type: @type, constraints: @attribute.constraints[:items] || []}, + @form, + {:list_value, this_value}, + @name <> "[#{index}]", + @id <> "_#{index}", + @union_type + )} +
+ +