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

[PR] Adding drag n drop and index-based reordering #345

Merged
merged 101 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
7d5a8be
feat: Adding position field and functions to change the position afte…
LuchoTurtle Mar 30, 2023
9b1a7f5
feat: Trying to get the highlight to work on the client of Phoenix. #145
LuchoTurtle Mar 31, 2023
a4ab130
fix: Fixing sorting on the client side. #145
LuchoTurtle Mar 31, 2023
5a4a5f3
feat: Switching positions between items is now persisted in the datab…
LuchoTurtle Mar 31, 2023
492478c
fix: Tags are part of the <li> so they can be dragged as well. #145
LuchoTurtle Mar 31, 2023
21cc6e5
fix: Undefined IDs don't create `updateIndexes` event. #145
LuchoTurtle Apr 3, 2023
aa75dcc
feat: Adding tests. Coverage back to 100%. #145
LuchoTurtle Apr 3, 2023
2c44131
feat: Add dot lines. #145
LuchoTurtle Apr 3, 2023
35a6b05
fix: Renaming events. #145
LuchoTurtle Apr 3, 2023
6ce28fb
fix: Mix format. #145
LuchoTurtle Apr 3, 2023
e1d4610
feat: Add section on build it. #145
LuchoTurtle Apr 4, 2023
83cc8d2
fix: Fixing typos. #145
LuchoTurtle Apr 4, 2023
eee8d49
fix: Running mix format.
LuchoTurtle Apr 4, 2023
7c0a29c
Merge branch 'main' into drag-drop_fix#145
LuchoTurtle Apr 4, 2023
83aaf88
tidy Reorder intro section of BUILDIT.md #145
nelsonic Apr 10, 2023
82934f6
reword reordering section of BUILDIT.md #145
nelsonic Apr 12, 2023
35bf05c
create lists and list_items schemas for #145 ref: https://github.com/…
nelsonic Apr 16, 2023
aad5a7b
rename list.text to list.name for clarity + add list_test.exs #145
nelsonic Apr 16, 2023
67683ad
add basic test for add_list_item/3 #145
nelsonic Apr 16, 2023
b198dc1
assert list_item.item_id == item.id #145
nelsonic Apr 16, 2023
cb2086d
mix format #145
nelsonic Apr 18, 2023
d3b8117
add test to confirm item can be added to multiple lists #145
nelsonic Apr 18, 2023
1817c31
set server: true and log level: :warn in test.exs fixes https://githu…
nelsonic Apr 21, 2023
26b538b
mix format: 999999.999 -> 999_999.999
nelsonic Apr 21, 2023
5bd136b
add dummy data for testing lists & list_items closes https://github.…
nelsonic Apr 22, 2023
ebb5a94
mix format !! #358
nelsonic Apr 22, 2023
5b7a2ac
fix failing tests
nelsonic May 19, 2023
6694439
mix format
nelsonic May 19, 2023
0a95402
Merge branch 'main' into drag-drop_fix#145
LuchoTurtle May 24, 2023
6267523
fix: Fix dragging when trying to update timer. #145
LuchoTurtle May 24, 2023
8bd145e
fix: Fixing timezone displaying on timers. #145
LuchoTurtle May 24, 2023
13da899
fix: Fix undefined behaviour after fixing timer. #145
LuchoTurtle May 24, 2023
c8828bd
Merge branch 'main' into drag-drop_fix#145
LuchoTurtle May 24, 2023
54aff67
Merge branch 'main' into drag-drop_fix#145
nelsonic Jul 18, 2023
e864a85
Tidy "Enter for tags" tests
nelsonic Jul 18, 2023
93df1b9
mix format
nelsonic Jul 18, 2023
fa0b79e
rename list.name to list.text for consistentcy #356
nelsonic Aug 7, 2023
fa375cf
define create_default_lists/1 function for #356
nelsonic Aug 7, 2023
beae4f7
get_list_by_text!/2 #356
nelsonic Aug 7, 2023
e41b92f
Simplify add_items_to_all_list/1 for #356
nelsonic Aug 9, 2023
a8e9234
revert the addition of :position field to items #345 ... 130 tests, 6…
nelsonic Aug 9, 2023
b9a2d1e
add newly created item to the "All" list #356
nelsonic Aug 9, 2023
42ee078
Add Decimal package to accurately handle decimal precision when calcu…
nelsonic Aug 11, 2023
9620d2f
Update SQL in items_with_timers/1 to fetch list_items.position ref: h…
nelsonic Aug 11, 2023
444757a
update "Drag and Drop item" test to use new get_list_item_position/1 …
nelsonic Aug 11, 2023
f6157d7
add tests for reordering functions #145 https://youtu.be/capV8asfWnc?…
nelsonic Aug 11, 2023
e46c80e
mix format #145 #356 #345
nelsonic Aug 11, 2023
ff12790
replace manual install (via curl) of flyctl with "uses: superfly/flyc…
nelsonic Aug 11, 2023
61611ec
remove redundant flyctl install from pr.yml https://github.com/dwyl/m…
nelsonic Aug 12, 2023
5288630
Link to lists docs in /book ref https://github.com/dwyl/mvp/issues/350
nelsonic Aug 12, 2023
56d92d5
lowercase "All" to "all" for #356
nelsonic Aug 13, 2023
f0fdebb
attempting to debug move_items for reordering #145
nelsonic Aug 14, 2023
b206b2a
add li_id and position to accummulate_item_timers/1 test for #145
nelsonic Aug 14, 2023
02cc7d0
mix format #145 #356 #345
nelsonic Aug 14, 2023
2df4cb7
SELECT COUNT(DISTINCT li.item_id) FROM list_items li #145
nelsonic Aug 14, 2023
eade158
add config :pre_commit to run "mix format" on commit #145
nelsonic Aug 14, 2023
5144a15
mix format --check-formatted #145
nelsonic Aug 14, 2023
5103ee0
remove /test directory from mix format inputs: https://github.com/dwy…
nelsonic Aug 15, 2023
1ccf28b
use Useful v1.12.1 in MVP ref: https://github.com/dwyl/useful/issues/…
nelsonic Aug 15, 2023
02f8706
Merge branch 'main' into drag-drop_fix#145
nelsonic Aug 15, 2023
76ba302
fix merge conflicts in mix.lock #406
nelsonic Aug 15, 2023
c39d6fb
temporarily disable the Review Apps Script ref: #407
nelsonic Aug 15, 2023
8ecdcc1
temporarily disable API tests which rely on review app ... #407 🙄
nelsonic Aug 15, 2023
29a1c48
Reordering *WORKING* but the "old" way ... #145
nelsonic Aug 16, 2023
c5521fb
Merge branch 'main' into drag-drop_fix#145
nelsonic Aug 16, 2023
509e99f
rename get_person_lists/1 to get_lists_for_person/1 for clarity #356
nelsonic Aug 17, 2023
1dcbe3c
[WiP] working on a simplified add_list_item/3 function for #356
nelsonic Aug 21, 2023
9a6e7b4
commit unstaged changes ...
nelsonic Aug 21, 2023
e1f996e
dramatically simplify add_list_item/3 as described in https://github.…
nelsonic Aug 21, 2023
9716b3c
test add_all_items_to_all_list_for_person_id/1 passing #356 #145
nelsonic Aug 22, 2023
07ac039
remove lib/app/list_BAK.ex as all functions have been simplified #356
nelsonic Aug 22, 2023
2f43ab9
rm lib/app/list_item_BAK as all functions simplified #356 #145
nelsonic Aug 22, 2023
e4aebcf
simplify add_all_items_to_all_list_for_person_id/1 to only accept per…
nelsonic Aug 22, 2023
fa6dcff
sadly, the tests are failng for #145 due to a mismatch in types ... 😢
nelsonic Aug 23, 2023
24cad7d
WiP: havnt fixed the tests yet but cant git push until I rebase ... 🙄
nelsonic Aug 23, 2023
9573f49
create put_cid/1 function for #410
nelsonic Aug 24, 2023
666c198
invoke Item.update_all_items_cid/0 from seeds.exs #410
nelsonic Aug 27, 2023
61cca86
revert to old version of accumulate_item_timers/1 so item_tests.exs a…
nelsonic Aug 27, 2023
ac23575
split put_cid/1 into a separate file to be reused in both items and l…
nelsonic Aug 27, 2023
9eb4138
use put_cid/1 in lists #410
nelsonic Aug 27, 2023
14a2110
add cid to list schema #410
nelsonic Aug 27, 2023
22d354e
only 2 tests faling ... #410
nelsonic Aug 27, 2023
d795b1f
List reorg WORKING with cids!!! #145 #356 #410 (but two tests still f…
nelsonic Aug 27, 2023
e54e3d1
fix failing test for accummulate_item_timers/1 #145 #356 #410
nelsonic Aug 27, 2023
c8c9c5c
tests passing again. reorg with cids works a charm. #145 #356 #410
nelsonic Aug 30, 2023
64ed1aa
create test/app/cid_test.exs to test put_cid/1 #410
nelsonic Aug 31, 2023
f46ded4
Merge branch 'main' into drag-drop_fix#145
nelsonic Aug 31, 2023
08844d3
fix failing stats tests ref: https://github.com/dwyl/mvp/pull/345/#is…
nelsonic Aug 31, 2023
7f91403
add guard status?/1 function to avoid nil cases fixes #412
nelsonic Aug 31, 2023
7ef0509
invoke App.ListItems.add_all_items_to_all_list_for_person_id/1 in mou…
nelsonic Aug 31, 2023
a386f98
mix format!! 🤦‍♂️
nelsonic Aug 31, 2023
39a758c
remove list_items schema while maintaining all functionality #413
nelsonic Sep 5, 2023
80c81c3
shift reordering docs to https://dwyl.github.io/book/mvp/18-reorderin…
nelsonic Sep 7, 2023
36f85c6
rename updateIndexes to update_list_seq for clarity #145
nelsonic Sep 7, 2023
4d45753
remove debug code from lib/app_web/live/app_live.html.heex #145
nelsonic Sep 7, 2023
fcb45fd
Fix numbering in BUILDIT.md #145
nelsonic Sep 7, 2023
102aaa6
Merge branch 'main' into drag-drop_fix#145
nelsonic Sep 7, 2023
bce6945
add test for update_list_seq/3 #145
nelsonic Sep 7, 2023
a7408a1
invoke App.Person.get_person_id from #415
nelsonic Sep 7, 2023
630255e
remove remaining dbg code #145
nelsonic Sep 7, 2023
6362525
remove more debug code ... #145
nelsonic Sep 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading