Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sorting for key value fields #3299

Merged
merged 13 commits into from
Nov 1, 2024
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Please read the [RELEASE.MD](./RELEASE.MD)
</tr>
</table>

[Become a sponsor ](https://github.com/sponsors/adrianthedev)
[Become a sponsor](mailto:[email protected])


![Alt](https://repobeats.axiom.co/api/embed/1481a6a259064f02a7936470d12a50802a9c98a4.svg "Repobeats analytics image")
Expand All @@ -120,3 +120,10 @@ Please read the [RELEASE.MD](./RELEASE.MD)
[Get a box of waffles and some of the best app monitoring from Appsignal](https://appsignal.com/r/93dbe69bfb) 🧇

[Get $100 in credits from Digital Ocean](https://www.digitalocean.com/?refcode=efc1fe881d74&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) 💸

## Other Open Source Work

- [`active_storage-blurhash`](https://github.com/avo-hq/active_storage-blurhash) - A plug-n-play [blurhash](https://blurha.sh/) integration for images stored in ActiveStorage
- [`class_variants`](https://github.com/avo-hq/class_variants) - Easily configure styles and apply them as classes. Very useful when you're implementing Tailwind CSS components and call them with different states.
- [`prop_initializer`](https://github.com/avo-hq/prop_initializer) - A flexible tool for defining properties on Ruby classes.
- [`stimulus-confetti`](https://github.com/avo-hq/stimulus-confetti) - The easiest way to add confetti to your StimulusJS app
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<%= @field.value_label %>
</div>
<% if @view.form? %>
<div class="flex items-center justify-center p-2 px-3 border-l border-gray-600">
<div class="flex items-center justify-center p-2 px-[48px] border-l border-gray-600">
<a href="javascript:void(0);"
title="<%= @field.action_text %>"
data-tippy="tooltip"
Expand Down
84 changes: 71 additions & 13 deletions app/javascript/js/controllers/fields/key_value_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import * as DOMPurify from 'dompurify'
import { Controller } from '@hotwired/stimulus'
import { castBoolean } from '../../helpers/cast_boolean'
import Sortable from 'sortablejs'

export default class extends Controller {
static targets = ['input', 'controller', 'rows']
Expand Down Expand Up @@ -46,6 +47,24 @@ export default class extends Controller {
this.updateKeyValueComponent()
}

moveKey(oldIndex, newIndex) {
if (!this.options.editable) return

this.fieldValue = this.moveElement(this.fieldValue, oldIndex, newIndex)

this.updateTextareaInput()
this.updateKeyValueComponent()
}

moveElement(arr, fromIndex, toIndex) {
return arr.map((item, index) => {
if (index === toIndex) return arr[fromIndex]
if (index === fromIndex) return arr[toIndex]

return item
})
}

focusLastRow() {
return this.rowsTarget.querySelector('.flex.key-value-row:last-child .key-value-input-key').focus()
}
Expand Down Expand Up @@ -85,26 +104,35 @@ export default class extends Controller {
index++
})
this.rowsTarget.innerHTML = result
this.#initDragNDrop()
window.initTippy()
}

#initDragNDrop() {
const vm = this
// eslint-disable-next-line no-new
new Sortable(this.rowsTarget, {
animation: 150,
handle: '[data-control="dnd-handle"]',
onUpdate(event) {
vm.moveKey(event.oldIndex, event.newIndex)
},
})
}

interpolatedRow(key, value, index) {
let result = `<div class="flex key-value-row">
let result = '<div class="flex key-value-row">'

result += `
${this.inputField('key', index, key, value)}
${this.inputField('value', index, key, value)}`
${this.inputField('value', index, key, value)}
`

if (this.options.editable) {
result += `<a
href="javascript:void(0);"
data-key-value-index-param="${index}"
data-action="click->key-value#deleteRow"
title="${this.options.delete_text}"
data-tippy="tooltip"
data-button="delete-row"
tabindex="-1"
${this.options.disable_deleting_rows ? "disabled='disabled'" : ''}
class="flex items-center justify-center p-2 px-3 border-none ${this.options.disable_deleting_rows ? 'cursor-not-allowed' : ''}"
><svg class="pointer-events-none text-gray-500 h-5 hover:text-gray-500" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg></a>`
result += this.upDownButtons(index)
result += this.deleteButton(index)
}

result += '</div>'

return result
Expand All @@ -123,6 +151,36 @@ export default class extends Controller {
/>`
}

deleteButton(index) {
return `<a
href="javascript:void(0);"
data-key-value-index-param="${index}"
data-action="click->key-value#deleteRow"
title="${this.options.delete_text}"
data-tippy="tooltip"
data-button="delete-row"
tabindex="-1"
${this.options.disable_deleting_rows ? "disabled='disabled'" : ''}
class="flex items-center justify-center p-2 px-3 border-none ${this.options.disable_deleting_rows ? 'cursor-not-allowed' : ''}"
><svg class="pointer-events-none text-gray-500 h-5 hover:text-gray-500" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg></a>`
}

upDownButtons(index) {
return `<div class="flex flex-col">
<a
href="javascript:void(0);"
data-key-value-index-param="${index}"
data-control="dnd-handle"
title="reorder"
data-tippy="tooltip"
tabindex="-1"
class="flex items-center justify-center py-0 px-2 border-none h-full ${this.options.disable_deleting_rows ? 'cursor-not-allowed' : ''}"
>
<svg class="pointer-events-none text-gray-500 h-4 hover:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
</a>
</div>`
}

setOptions() {
let fieldOptions

Expand Down
3 changes: 2 additions & 1 deletion lib/avo/fields/key_value_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ def options
}
end

def fill_field(record, key, value, params)
def fill_field(record, key, value, _params)
begin
new_value = JSON.parse(value)
rescue
new_value = {}
end

record.send(:"#{key}_will_change!")
record.send(:"#{key}=", new_value)

record
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"postcss-nested-ancestors": "^3.0.0",
"postcss-preset-env": "^8.5.1",
"regenerator-runtime": "^0.13.11",
"sortablejs": "^1.15.3",
"stimulus-rails-nested-form": "^4.1.0",
"stimulus-textarea-autogrow": "^4.1.0",
"stimulus-use": "^0.50.0",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5339,6 +5339,11 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"

sortablejs@^1.15.3:
version "1.15.3"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.3.tgz#033668db5ebfb11167d1249ab88e748f27959e29"
integrity sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==

source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
Expand Down
Loading