Skip to content

Commit

Permalink
Merge pull request #345 from dwyl/drag-drop_fix#145
Browse files Browse the repository at this point in the history
[PR] Adding drag n drop and index-based reordering
  • Loading branch information
nelsonic authored Sep 7, 2023
2 parents 699e6cc + 6362525 commit b8aee0e
Show file tree
Hide file tree
Showing 28 changed files with 1,486 additions and 529 deletions.
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: [
"*.{heex,ex,exs}",
"{config,lib,test}/**/*.{heex,ex,exs}",
"{config,lib}/**/*.{heex,ex,exs}",
"priv/*/seeds.exs"
],
line_length: 80
Expand Down
109 changes: 93 additions & 16 deletions BUILDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ With that in place, let's get building!
- [15.2 Changing how the timer datetime is displayed](#152-changing-how-the-timer-datetime-is-displayed)
- [15.3 Persisting the adjusted timezone](#153-persisting-the-adjusted-timezone)
- [15.4 Adding test](#154-adding-test)
- [16. Run the _Finished_ MVP App!](#16-run-the-finished-mvp-app)
- [16.1 Run the Tests](#161-run-the-tests)
- [16.2 Run The App](#162-run-the-app)
- [16. `Lists`](#16-lists)
- [17. Reordering `items` Using Drag \& Drop](#17-reordering-items-using-drag--drop)
- [18. Run the _Finished_ MVP App!](#18-run-the-finished-mvp-app)
- [18.1 Run the Tests](#181-run-the-tests)
- [18.2 Run The App](#182-run-the-app)
- [Thanks!](#thanks)


Expand Down Expand Up @@ -3593,7 +3595,13 @@ We are showing each timer whenever an `item` is being edited.
required="required"
name="timer_start"
id={"#{changeset.data.id}_start"}
value={changeset.data.start}
value={
NaiveDateTime.add(
changeset.data.start,
@hours_offset_fromUTC,
:hour
)
}
/>
</div>
<div class="flex flex-row items-center">
Expand All @@ -3602,7 +3610,17 @@ We are showing each timer whenever an `item` is being edited.
type="text"
name="timer_stop"
id={"#{changeset.data.id}_stop"}
value={changeset.data.stop}
value={
if is_nil(changeset.data.stop) do
changeset.data.stop
else
NaiveDateTime.add(
changeset.data.stop,
@hours_offset_fromUTC,
:hour
)
end
}
/>
</div>
<input
Expand Down Expand Up @@ -4861,7 +4879,7 @@ which is a PostgreSQL GUI.
If you don't have this installed,
[we highly recommend you doing so](https://github.com/dwyl/learn-postgresql/issues/43#issuecomment-469000357).
<img width="1824" alt="dbeaver" src="https://user-images.githubusercontent.com/17494745/211629270-996e6c4a-8322-49b4-9ef6-7be2335ccfb7.png">
<img width="1824" alt="papertrail_versions" src="https://user-images.githubusercontent.com/17494745/211629270-996e6c4a-8322-49b4-9ef6-7be2335ccfb7.png">
As you can see, update/insert events are being tracked,
with the corresponding `person_id` (in `originator_id`),
Expand Down Expand Up @@ -5785,11 +5803,56 @@ we expect the persisted value to be
one hour *less* than what the person inputted.
# 16. Run the _Finished_ MVP App!
# 16. `Lists`
In preparation for the next set of features in the `MVP`,
we added `lists`
which are simply a collection of `items`.
Please see:
[book/mvp/lists](https://dwyl.github.io/book/mvp/16-lists.html)
We didn't add a lot of code for `lists`
there is currently no way for the `person`
to create a `new list`
or "move" `items` between `lists`.
If you want to help with defining the interface,
please comment on the issue:
[dwyl/mvp#365](https://github.com/dwyl/mvp/issues/365)
# 17. Reordering `items` Using Drag & Drop
At present `people` using the `App`
can only add new `items` to a stack
where the newest is on top; no ordering.
`people` that tested the `MVP`
noted that the ability to **reorder `items`**
was an **_essential_ feature**:
[dwyl/mvp#145](https://github.com/dwyl/mvp/issues/145)
So in this step we are going to
add the ability to organize `items`.
We will implement reordering using
**drag and drop**!
And by using `Phoenix LiveView`,
**other people** will also be able
to **see the changes in real time**!
For _all_ the detail implementing this feature,
please see:
[book/mvp/reordering](https://dwyl.github.io/book/mvp/18-reordering.html)
# 18. Run the _Finished_ MVP App!
With all the code saved, let's run the tests one more time.
## 16.1 Run the Tests
## 18.1 Run the Tests
In your terminal window, run:
Expand All @@ -5802,23 +5865,37 @@ mix c
You should see output similar to the following:
```sh
Finished in 0.7 seconds (0.1s async, 0.5s sync)
85 tests, 0 failures
Finished in 1.5 seconds (1.4s async, 0.1s sync)
117 tests, 0 failures
Randomized with seed 947856
----------------
COV FILE LINES RELEVANT MISSED
100.0% lib/app/item.ex 245 34 0
100.0% lib/app/timer.ex 97 16 0
100.0% lib/app_web/controllers/auth_controller. 35 9 0
100.0% lib/app_web/live/app_live.ex 186 57 0
[TOTAL] 100.0%
100.0% lib/api/item.ex 218 56 0
100.0% lib/api/tag.ex 101 24 0
100.0% lib/api/timer.ex 152 40 0
100.0% lib/app/color.ex 90 1 0
100.0% lib/app/item.ex 415 62 0
100.0% lib/app/item_tag.ex 12 1 0
100.0% lib/app/tag.ex 108 18 0
100.0% lib/app/timer.ex 452 84 0
100.0% lib/app_web/controllers/auth_controller. 26 4 0
100.0% lib/app_web/controllers/init_controller. 41 6 0
100.0% lib/app_web/controllers/tag_controller.e 77 25 0
100.0% lib/app_web/live/app_live.ex 476 132 0
100.0% lib/app_web/live/stats_live.ex 77 21 0
100.0% lib/app_web/router.ex 49 9 0
100.0% lib/app_web/views/error_view.ex 59 12 0
0.0% lib/app_web/views/profile_view.ex 3 0 0
0.0% lib/app_web/views/tag_view.ex 3 0 0
[TOTAL] 100.0%
----------------
```
All tests pass and we have **`100%` Test Coverage**.
This reminds us just how few _relevant_ lines of code there are in the MVP!
## 16.2 Run The App
## 18.2 Run The App
In your second terminal tab/window, run:
Expand Down
14 changes: 14 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,17 @@ input[type=radio].has-error:not(.phx-no-feedback) {
}

[x-cloak] { display: none !important; }


/* For the drag and drop feature */
.cursor-grab {
cursor: grab;
}

.cursor-grabbing {
cursor: grabbing;
}

.bg-teal-300 {
background-color: #5eead4;
}
101 changes: 95 additions & 6 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,103 @@ import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

// Drag and drop highlight handlers
window.addEventListener("phx:highlight", (e) => {
document.querySelectorAll("[data-highlight]").forEach(el => {
if(el.id == e.detail.id) {
liveSocket.execJS(el, el.getAttribute("data-highlight"))
}
})
})

// Item id of the destination in the DOM
let itemId_to;

let Hooks = {}
Hooks.Items = {
mounted() {
const hook = this

this.el.addEventListener("highlight", e => {
hook.pushEventTo("#items", "highlight", {id: e.detail.id})
// console.log('highlight', e.detail.id)
})

this.el.addEventListener("remove-highlight", e => {
hook.pushEventTo("#items", "removeHighlight", {id: e.detail.id})
// console.log('remove-highlight', e.detail.id)
})

this.el.addEventListener("dragoverItem", e => {
// console.log("dragoverItem", e.detail)
const currentItemId = e.detail.currentItem.id
const selectedItemId = e.detail.selectedItemId
if( currentItemId != selectedItemId) {
hook.pushEventTo("#items", "dragoverItem", {currentItemId: currentItemId, selectedItemId: selectedItemId})
itemId_to = e.detail.currentItem.dataset.id
}
})

this.el.addEventListener("update-indexes", e => {
const item_id = e.detail.fromItemId
const list_ids = get_list_item_cids()
console.log("update-indexes", e.detail, "list: ", list_ids)
// Check if both "from" and "to" are defined
if(item_id && itemId_to && item_id != itemId_to) {
hook.pushEventTo("#items", "update_list_seq",
{seq: list_ids})
}

itemId_to = null;
})
}
}

/**
* `get_list_item_ids/0` retrieves the full `list` of visible `items` form the DOM
* and returns a String containing the IDs as a space-separated list e.g: "1 2 3 42 71 93"
* This is used to determine the `position` of the `item` that has been moved.
*/
function get_list_item_cids() {
console.log("invoke get_list_item_ids")
const lis = document.querySelectorAll("label[phx-value-cid]");
return Object.values(lis).map(li => {
return li.attributes["phx-value-cid"].nodeValue
}).join(",")
}

window.addEventListener("phx:remove-highlight", (e) => {
document.querySelectorAll("[data-highlight]").forEach(el => {
if(el.id == e.detail.id) {
liveSocket.execJS(el, el.getAttribute("data-remove-highlight"))
}
})
})

window.addEventListener("phx:dragover-item", (e) => {
console.log("phx:dragover-item", e.detail)
const selectedItem = document.querySelector(`#${e.detail.selected_item_id}`)
const currentItem = document.querySelector(`#${e.detail.current_item_id}`)

const items = document.querySelector('#items')
const listItems = [...document.querySelectorAll('.item')]

if(listItems.indexOf(selectedItem) < listItems.indexOf(currentItem)){
items.insertBefore(selectedItem, currentItem.nextSibling)
}

if(listItems.indexOf(selectedItem) > listItems.indexOf(currentItem)){
items.insertBefore(selectedItem, currentItem)
}
})

// liveSocket related setup:

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

let liveSocket = new LiveSocket("/live", Socket, {
Expand All @@ -24,12 +119,6 @@ let liveSocket = new LiveSocket("/live", Socket, {
}
})


// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

// connect if there are any LiveViews on the page
liveSocket.connect()

Expand Down
9 changes: 7 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Config

config :app,
ecto_repos: [App.Repo]
ecto_repos: [App.Repo],
# rickaard.se/blog/how-to-only-run-some-code-in-production-with-phoenix-and-elixir
env: config_env()

# Configures the endpoint
config :app, AppWeb.Endpoint,
Expand Down Expand Up @@ -50,6 +52,9 @@ import_config "#{config_env()}.exs"
# https://hexdocs.pm/joken/introduction.html#usage
config :joken, default_signer: System.get_env("SECRET_KEY_BASE")

#
# https://github.com/dwyl/auth_plug
config :auth_plug,
api_key: System.get_env("AUTH_API_KEY")

# https://github.com/dwyl/cid#how
config :excid, base: :base58
5 changes: 5 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,8 @@ config :phoenix, :stacktrace_depth, 20

# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime

# github.com/dwyl/elixir-pre-commit
config :pre_commit,
commands: ["format", "c"],
verbose: true
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ config :app, AppWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base:
"aEkLhne04vW3X5PM63O85Ie57c+KoT1z5bl0TdtBE1veN8BbER7MpOgZ6FgD7dWu",
# github.com/dwyl/mvp/issues/359
server: false

# Print only warnings and errors during test
Expand Down
25 changes: 25 additions & 0 deletions lib/app/cid.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule App.Cid do
@moduledoc """
Helper functions for adding `cid` to records transparently in a changeset pipeline.
"""

@doc """
`put_cid/1` as its' name suggests puts the `cid` for the record into the `changeset`.
This is done transparently so nobody needs to _think_ about cids.
"""
def put_cid(changeset) do
# don't add a cid to a changeset that already has one
if Map.has_key?(changeset.changes, :cid) do
changeset
else
# Only add cid to changeset that has :name i.e. list.name or :text i.e. item.text
if Map.has_key?(changeset.changes, :name) ||
Map.has_key?(changeset.changes, :text) do
cid = Cid.cid(changeset.changes)
%{changeset | changes: Map.put(changeset.changes, :cid, cid)}
else
changeset
end
end
end
end
Loading

0 comments on commit b8aee0e

Please sign in to comment.