diff --git a/.formatter.exs b/.formatter.exs
index 1271728e..7836e828 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -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
diff --git a/BUILDIT.md b/BUILDIT.md
index f1922bff..52673f83 100644
--- a/BUILDIT.md
+++ b/BUILDIT.md
@@ -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)
@@ -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
+ )
+ }
/>
@@ -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
+ }
/>
+
As you can see, update/insert events are being tracked,
with the corresponding `person_id` (in `originator_id`),
@@ -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:
@@ -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:
diff --git a/assets/css/app.css b/assets/css/app.css
index eca5b057..ed7dc4da 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -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;
+}
\ No newline at end of file
diff --git a/assets/js/app.js b/assets/js/app.js
index 27314ea5..9797d5b1 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -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, {
@@ -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()
diff --git a/config/config.exs b/config/config.exs
index dc81496e..a82cf8dd 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -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,
@@ -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
diff --git a/config/dev.exs b/config/dev.exs
index 6f716d6d..5b420175 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -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
diff --git a/config/test.exs b/config/test.exs
index a7275cd0..4bd365ff 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -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
diff --git a/lib/app/cid.ex b/lib/app/cid.ex
new file mode 100644
index 00000000..45a5adb8
--- /dev/null
+++ b/lib/app/cid.ex
@@ -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
diff --git a/lib/app/item.ex b/lib/app/item.ex
index 668b7f8a..43a1e742 100644
--- a/lib/app/item.ex
+++ b/lib/app/item.ex
@@ -10,6 +10,7 @@ defmodule App.Item do
@derive {Jason.Encoder,
except: [:__meta__, :__struct__, :timer, :inserted_at, :updated_at]}
schema "items" do
+ field :cid, :string
field :person_id, :integer
field :status, :integer
field :text, :string
@@ -23,8 +24,9 @@ defmodule App.Item do
@doc false
def changeset(item, attrs) do
item
- |> cast(attrs, [:person_id, :status, :text])
+ |> cast(attrs, [:cid, :person_id, :status, :text])
|> validate_required([:text, :person_id])
+ |> App.Cid.put_cid()
end
def changeset_with_tags(item, attrs) do
@@ -36,10 +38,11 @@ defmodule App.Item do
item
|> cast(attrs, [:person_id, :status, :text])
|> validate_required([:person_id])
+ |> App.Cid.put_cid()
end
@doc """
- Creates an `item`.
+ `create_item/1` creates an `item`.
## Examples
@@ -54,6 +57,7 @@ defmodule App.Item do
%Item{}
|> changeset(attrs)
|> PaperTrail.insert(originator: %{id: Map.get(attrs, :person_id, 0)})
+ |> App.List.add_papertrail_item_to_all_list()
end
@doc """
@@ -71,6 +75,7 @@ defmodule App.Item do
%Item{}
|> changeset_with_tags(attrs)
|> PaperTrail.insert(originator: %{id: Map.get(attrs, :person_id, 0)})
+ |> App.List.add_papertrail_item_to_all_list()
end
@doc """
@@ -134,7 +139,6 @@ defmodule App.Item do
"""
def list_items do
Item
- |> order_by(desc: :inserted_at)
|> where([i], is_nil(i.status) or i.status != 6)
|> Repo.all()
end
@@ -186,12 +190,19 @@ defmodule App.Item do
|> Repo.update()
end
+ def all_items_for_person(person_id) do
+ Item
+ |> where(person_id: ^person_id)
+ |> Repo.all()
+ end
+
# 🐲 H E R E B E D R A G O N S! 🐉
# ⏳ Working with Time is all Dragons! 🙄
# 👩💻 Feedback/Pairing/Refactoring Welcome! 🙏
@doc """
`items_with_timers/1` Returns a List of items with the latest associated timers.
+ This list is ordered with the position that is detailed inside the Items schema.
## Examples
@@ -203,24 +214,33 @@ defmodule App.Item do
"""
#
def items_with_timers(person_id \\ 0) do
+ all_list = App.List.get_all_list_for_person(person_id)
+ seq = App.List.get_list_seq(all_list)
+
sql = """
- SELECT i.id, i.text, i.status, i.person_id, t.start, t.stop, t.id as timer_id FROM items i
- FULL JOIN timers as t ON t.item_id = i.id
- WHERE i.person_id = $1 AND i.status IS NOT NULL
+ SELECT i.id, i.cid, i.text, i.status, i.person_id, i.updated_at,
+ t.start, t.stop, t.id as timer_id
+ FROM items i
+ FULL JOIN timers AS t ON t.item_id = i.id
+ WHERE i.cid = any($1)
+ AND i.status IS NOT NULL
+ AND i.text IS NOT NULL
ORDER BY timer_id ASC;
"""
values =
- Ecto.Adapters.SQL.query!(Repo, sql, [person_id])
+ Ecto.Adapters.SQL.query!(Repo, sql, [seq])
|> map_columns_to_values()
items_tags =
list_person_items(person_id)
|> Enum.reduce(%{}, fn i, acc -> Map.put(acc, i.id, i) end)
- accumulate_item_timers(values)
+ accumulate_item_timers(values, seq)
|> Enum.map(fn t ->
- Map.put(t, :tags, items_tags[t.id].tags)
+ if t != nil do
+ Map.put(t, :tags, items_tags[t.id].tags)
+ end
end)
end
@@ -299,11 +319,11 @@ defmodule App.Item do
And having multiple timers is the *only* way to achieve that.
If you can think of a better way of achieving the same result,
- please share: https://github.com/dwyl/app-mvp-phoenix/issues/103
+ please share: github.com/dwyl/app-mvp-phoenix/issues/103
This function *relies* on the list of items being ordered by timer_id ASC
because it "pops" the last timer and ignores it to avoid double-counting.
"""
- def accumulate_item_timers(items_with_timers) do
+ def accumulate_item_timers(items_with_timers, seq) do
# e.g: %{0 => 0, 1 => 6, 2 => 5, 3 => 24, 4 => 7}
timer_id_diff_map = map_timer_diff(items_with_timers)
@@ -331,20 +351,51 @@ defmodule App.Item do
end)}
end)
- # creates a nested map: %{ item.id: %{id: 1, text: "my item", etc.}}
- Map.new(items_with_timers, fn item ->
- time_elapsed = Map.get(item_id_timer_diff_map, item.id)
+ # creates a nested map: %{ item.cid: %{id: 1, text: "my item", etc.}}
+ cid_item_map =
+ Map.new(items_with_timers, fn item ->
+ time_elapsed = Map.get(item_id_timer_diff_map, item.id)
+
+ start =
+ if is_nil(item.start),
+ do: nil,
+ else: NaiveDateTime.add(item.start, -time_elapsed)
+
+ {item.cid, %{item | start: start}}
+ end)
+
+ # return the list of items in the order of seq
+ Enum.map(seq, fn cid -> cid_item_map[cid] end)
+ end
- start =
- if is_nil(item.start),
- do: nil,
- else: NaiveDateTime.add(item.start, -time_elapsed)
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+ # Below this point is item transition code that will be DELETED! #
+ # We just need it to update all existing items to add cid ... #
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ @doc """
+ `update_all_items_cid/0` updates all `item` records with a `cid` value.
+ This will not be needed once all records are transitioned.
+ """
+ def update_all_items_cid do
+ items = list_items()
+
+ Enum.each(items, fn i ->
+ # coveralls-ignore-start
+ unless Map.has_key?(i, :cid) do
+ item = %{
+ person_id: i.person_id,
+ status: i.status,
+ text: i.text,
+ id: i.id
+ }
+
+ i
+ |> changeset(Map.put(item, :cid, Cid.cid(item)))
+ |> Repo.update()
+ end
- {item.id, %{item | start: start}}
+ # coveralls-ignore-stop
end)
- # Return the list of items without duplicates and only the last/active timer:
- |> Map.values()
- # Sort list by item.id descending (ordered by timer_id ASC above) so newest item first:
- |> Enum.sort_by(fn i -> i.id end, :desc)
end
end
diff --git a/lib/app/list.ex b/lib/app/list.ex
new file mode 100644
index 00000000..a4d7c993
--- /dev/null
+++ b/lib/app/list.ex
@@ -0,0 +1,213 @@
+defmodule App.List do
+ require Logger
+ use Ecto.Schema
+ import Ecto.{Changeset, Query}
+ alias App.{Repo}
+ alias PaperTrail
+ alias __MODULE__
+
+ schema "lists" do
+ field :cid, :string
+ field :name, :string
+ field :person_id, :integer
+ field :seq, :string
+ field :sort, :integer
+ field :status, :integer
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(list, attrs) do
+ list
+ |> cast(attrs, [:name, :person_id, :seq, :sort, :status])
+ |> validate_required([:name, :person_id])
+ |> App.Cid.put_cid()
+ end
+
+ @doc """
+ `create_list/1` creates an `list`.
+
+ ## Examples
+
+ iex> create_list(%{name: "Personal Todo List"})
+ {:ok, %List{}}
+
+ iex> create_list(%{name: nil})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_list(attrs) do
+ %List{}
+ |> changeset(attrs)
+ |> PaperTrail.insert()
+ end
+
+ @doc """
+ `get_list!/1` gets the `list` record.
+
+ Raises `Ecto.NoResultsError` if the List does not exist.
+
+ ## Examples
+
+ iex> get_list!(17)
+ %List{}
+
+ iex> get_list!(420)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_list!(id) do
+ List
+ |> Repo.get!(id)
+ end
+
+ @doc """
+ `get_list_by_cid!/1` gets the `list` record by its' `cid`.
+
+ Raises `Ecto.NoResultsError` if the List does not exist.
+
+ ## Examples
+
+ iex> get_list!("cidhere")
+ %List{}
+
+ iex> get_list!(420)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_list_by_cid!(cid) do
+ List
+ |> where(cid: ^cid)
+ |> Repo.one!()
+ end
+
+ @doc """
+ `update_list/2` updates a list.
+
+ ## Examples
+
+ iex> update_list(list, %{name: "renamed list"})
+ {:ok, %List{}}
+
+ iex> update_list(list, %{name: nil})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_list(%List{} = list, attrs) do
+ list
+ |> List.changeset(attrs)
+ |> PaperTrail.update()
+ end
+
+ @doc """
+ `get_lists_for_person/1` gets all lists for a person by `person_id`.
+ """
+ def get_lists_for_person(person_id) do
+ List
+ |> where(person_id: ^person_id)
+ |> Repo.all()
+ end
+
+ @doc """
+ `get_list_by_name!/2` gets the `list` record by it's `name` attribute.
+ e.g: `get_list_by_name!("shopping", 42)`
+
+ Raises `Ecto.NoResultsError` if the List does not exist.
+
+ ## Examples
+
+ iex> get_list_by_name!("all", 1)
+ %List{}
+
+ iex> get_list_by_name!("¯\_(ツ)_/¯", 0)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_list_by_name!(name, person_id) do
+ Repo.get_by(List, name: name, person_id: person_id)
+ end
+
+ @doc """
+ get_all_list_for_person/1 gets or creates the "all" list for a given `person_id`
+ """
+ def get_all_list_for_person(person_id) do
+ # IO.inspect("get_all_list_for_person(person_id: #{person_id})")
+ all_list = get_list_by_name!("all", person_id)
+
+ if all_list == nil do
+ # doesn't exist, create it:
+ {:ok, %{model: list}} =
+ create_list(%{name: "all", person_id: person_id, status: 2})
+
+ list
+ else
+ all_list
+ end
+ end
+
+ def add_item_to_list(item_cid, list_cid, person_id) do
+ list = get_list_by_cid!(list_cid)
+ prev_seq = get_list_seq(list)
+ seq = [item_cid | prev_seq] |> Enum.join(",")
+ update_list(list, %{seq: seq, person_id: person_id})
+ end
+
+ def update_list_seq(list_cid, person_id, seq) do
+ list = get_list_by_cid!(list_cid)
+ update_list(list, %{seq: seq, person_id: person_id})
+ end
+
+ # feel free to refactor this to use pattern matching:
+ def add_papertrail_item_to_all_list(tuple) do
+ # extract the item from the tuple:
+ try do
+ {:ok, %{model: item}} = tuple
+ all_list = App.List.get_all_list_for_person(item.person_id)
+ add_item_to_list(item.cid, all_list.cid, item.person_id)
+ rescue
+ e ->
+ Logger.error(Exception.format(:error, e, __STACKTRACE__))
+ end
+
+ # return the original tuple as expected downstream:
+ tuple
+ end
+
+ def get_list_seq(list) do
+ if is_nil(list.seq) do
+ []
+ else
+ list.seq |> String.split(",")
+ end
+ end
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+ # Below this point is Lists transition code that will be DELETED! #
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ @doc """
+ `add_all_items_to_all_list_for_person_id/1` does *exactly* what its' name suggests.
+ Adds *all* the person's `items` to the `list_items.seq`.
+ """
+ def add_all_items_to_all_list_for_person_id(person_id) do
+ all_list = App.List.get_all_list_for_person(person_id)
+ all_items = App.Item.all_items_for_person(person_id)
+ prev_seq = get_list_seq(all_list)
+ # Add add each `item.id` to the sequence of item ids:
+ seq =
+ Enum.reduce(all_items, prev_seq, fn i, acc ->
+ # Avoid adding duplicates
+ if Enum.member?(acc, i.cid) do
+ acc
+ else
+ [i.cid | acc]
+ end
+ end)
+ |> Enum.uniq()
+ |> Enum.filter(fn cid -> cid != nil && cid != "" end)
+ |> Enum.join(",")
+
+ update_list(all_list, %{seq: seq})
+ end
+end
diff --git a/lib/app_web/controllers/auth_controller.ex b/lib/app_web/controllers/auth_controller.ex
index 3b7266e0..2a750f44 100644
--- a/lib/app_web/controllers/auth_controller.ex
+++ b/lib/app_web/controllers/auth_controller.ex
@@ -1,6 +1,5 @@
defmodule AppWeb.AuthController do
use AppWeb, :controller
-
import Phoenix.Component, only: [assign_new: 3]
def on_mount(:default, _params, %{"jwt" => jwt} = _session, socket) do
diff --git a/lib/app_web/controllers/init_controller.ex b/lib/app_web/controllers/init_controller.ex
index 60a93cb0..3d0d1aa5 100644
--- a/lib/app_web/controllers/init_controller.ex
+++ b/lib/app_web/controllers/init_controller.ex
@@ -5,6 +5,9 @@ defmodule AppWeb.InitController do
@env_required ~w/AUTH_API_KEY ENCRYPTION_KEYS SECRET_KEY_BASE DATABASE_URL/
def index(conn, _params) do
+ Logger.info("attempeting to update cid of all items ...")
+ App.Item.update_all_items_cid()
+
Logger.info("init attempting to check environment variables ... ")
conn
diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex
index 7be788fa..c6c884ef 100644
--- a/lib/app_web/live/app_live.ex
+++ b/lib/app_web/live/app_live.ex
@@ -3,13 +3,17 @@ defmodule AppWeb.AppLive do
use AppWeb, :live_view
use Timex
alias App.{Item, Person, Tag, Timer}
+ alias Phoenix.Socket.Broadcast
+ alias Phoenix.LiveView.JS
+
# run authentication on mount
on_mount(AppWeb.AuthController)
- alias Phoenix.Socket.Broadcast
@topic "live"
@stats_topic "stats"
+ defp get_list_cid(assigns), do: assigns[:list_cid]
+
@impl true
def mount(_params, _session, socket) do
# subscribe to the channel
@@ -17,6 +21,10 @@ defmodule AppWeb.AppLive do
AppWeb.Endpoint.subscribe(@stats_topic)
person_id = Person.get_person_id(socket.assigns)
+ # Create or Get the "all" list for the person_id
+ all_list = App.List.get_all_list_for_person(person_id)
+ # Temporary function to add All *existing* items to the "All" list:
+ App.List.add_all_items_to_all_list_for_person_id(person_id)
items = Item.items_with_timers(person_id)
tags = Tag.list_person_tags(person_id)
selected_tags = []
@@ -29,10 +37,10 @@ defmodule AppWeb.AppLive do
editing: nil,
filter: "active",
filter_tag: nil,
+ list_cid: all_list.cid,
tags: tags,
selected_tags: selected_tags,
text_value: draft_item.text || "",
-
# Offset from the client to UTC. If it's "1", it means we are one hour ahead of UTC.
hours_offset_fromUTC:
get_connect_params(socket)["hours_offset_fromUTC"] || 0
@@ -52,12 +60,16 @@ defmodule AppWeb.AppLive do
def handle_event("create", %{"text" => text}, socket) do
person_id = Person.get_person_id(socket.assigns)
- Item.create_item_with_tags(%{
- text: text,
- person_id: person_id,
- status: 2,
- tags: socket.assigns.selected_tags
- })
+ {:ok, %{model: _item}} =
+ Item.create_item_with_tags(%{
+ text: text,
+ person_id: person_id,
+ status: 2,
+ tags: socket.assigns.selected_tags
+ })
+
+ # Add this newly created `item` to the "All" list:
+ # App.ListItem.add_item_to_all_list(item)
draft = Item.get_draft_item(person_id)
Item.update_draft(draft, %{text: ""})
@@ -82,7 +94,13 @@ defmodule AppWeb.AppLive do
# need to restrict getting items to the people who own or have rights to access them!
item = Item.get_item!(Map.get(data, "id"))
- Item.update_item(item, %{status: status, person_id: person_id})
+
+ Item.update_item(item, %{
+ status: status,
+ person_id: person_id,
+ cid: item.cid
+ })
+
Timer.stop_timer_for_item_id(item.id)
AppWeb.Endpoint.broadcast(@topic, "update", :toggle)
@@ -121,8 +139,8 @@ defmodule AppWeb.AppLive do
socket
) do
case socket.assigns.tags do
- [] ->
- {:noreply, socket}
+ # [] ->
+ # {:noreply, socket}
_ ->
selected_tag =
@@ -248,6 +266,79 @@ defmodule AppWeb.AppLive do
end
end
+ @impl true
+ def handle_event("highlight", %{"id" => id}, socket) do
+ AppWeb.Endpoint.broadcast(@topic, "move_items", {:drag_item, id})
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("removeHighlight", %{"id" => id}, socket) do
+ AppWeb.Endpoint.broadcast(@topic, "move_items", {:drop_item, id})
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event(
+ "dragoverItem",
+ %{
+ "currentItemId" => current_item_id,
+ "selectedItemId" => selected_item_id
+ },
+ socket
+ ) do
+ AppWeb.Endpoint.broadcast(
+ @topic,
+ "move_items",
+ {:dragover_item, {current_item_id, selected_item_id}}
+ )
+
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event(
+ "update_list_seq",
+ %{"seq" => seq},
+ socket
+ ) do
+ list_cid = get_list_cid(socket.assigns)
+ person_id = App.Person.get_person_id(socket.assigns)
+ App.List.update_list_seq(list_cid, person_id, seq)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_info(
+ %Broadcast{
+ event: "move_items",
+ payload: {:dragover_item, {current_item_id, selected_item_id}}
+ },
+ socket
+ ) do
+ {:noreply,
+ push_event(socket, "dragover-item", %{
+ current_item_id: current_item_id,
+ selected_item_id: selected_item_id
+ })}
+ end
+
+ @impl true
+ def handle_info(
+ %Broadcast{event: "move_items", payload: {:drag_item, item_id}},
+ socket
+ ) do
+ {:noreply, push_event(socket, "highlight", %{id: item_id})}
+ end
+
+ @impl true
+ def handle_info(
+ %Broadcast{event: "move_items", payload: {:drop_item, item_id}},
+ socket
+ ) do
+ {:noreply, push_event(socket, "remove-highlight", %{id: item_id})}
+ end
+
@impl true
def handle_info(%Broadcast{event: "update", payload: payload}, socket) do
person_id = Person.get_person_id(socket.assigns)
@@ -299,9 +390,14 @@ defmodule AppWeb.AppLive do
def has_items?(items), do: length(items) > 1
# 2: uncategorised (when item are created), 3: active
- def active?(item), do: item.status == 2 || item.status == 3
- def done?(item), do: item.status == 4
- def archived?(item), do: item.status == 6
+ def status?(item), do: not is_nil(item) && Map.has_key?(item, :status)
+
+ def active?(item),
+ do:
+ (status?(item) && item.status == 2) || (status?(item) && item.status == 3)
+
+ def done?(item), do: status?(item) && item.status == 4
+ def archived?(item), do: status?(item) && item.status == 6
# Check if an item has an active timer
def started?(item) do
@@ -379,6 +475,9 @@ defmodule AppWeb.AppLive do
end
defp filter_items(items, filter, filter_tag) do
+ # avoid nil items mvp#412
+ items = Enum.reject(items, &is_nil/1)
+
items =
case filter do
"active" ->
diff --git a/lib/app_web/live/app_live.html.heex b/lib/app_web/live/app_live.html.heex
index ec9690fd..45a9d2a2 100644
--- a/lib/app_web/live/app_live.html.heex
+++ b/lib/app_web/live/app_live.html.heex
@@ -109,7 +109,10 @@
<% end %>