diff --git a/BUILDIT.md b/BUILDIT.md
index 52673f83..ce194dd9 100644
--- a/BUILDIT.md
+++ b/BUILDIT.md
@@ -825,7 +825,7 @@ defmodule App.TimerTest do
{:ok, timer} =
Timer.start(%{item_id: item.id, person_id: 1, start: seven_seconds_ago})
- # stop the timer based on it's item_id
+ # stop the timer based on it's item_id
Timer.stop_timer_for_item_id(item.id)
stopped_timer = Timer.get_timer!(timer.id)
@@ -3165,10 +3165,10 @@ everytime `Start/Resume` or `Stop` is called.
person_id = get_person_id(socket.assigns)
items = Item.items_with_timers(person_id)
- isEditingItem = socket.assigns.editing
+ is_editing_item = socket.assigns.editing
# If the item is being edited, we update the timer list of the item being edited.
- if isEditingItem do
+ if is_editing_item do
case payload do
{:start, item_id} ->
timers_list_changeset = Timer.list_timers_changesets(item_id)
@@ -5469,13 +5469,13 @@ to the `LiveView` server during the mounting phase.
Open `assets/js/app.js`
and locate the
-`let liveSocket = new LiveSocket()` variable.
+`let live_socket = new LiveSocket()` variable.
We are going to be changing the `params` attribute.
Change it to the following:
```js
params: {
- _csrf_token: csrfToken,
+ _csrf_token: csrf_token,
hours_offset_fromUTC: -new Date().getTimezoneOffset()/60
}
```
@@ -5927,4 +5927,4 @@ please let us know by starring the repo on GitHub! ⭐
-[![HitCount](https://hits.dwyl.com/dwyl/app-mvp-build.svg)](https://hits.dwyl.com/dwyl/app-mvp)
+[![HitCount](https://hits.dwyl.com/dwyl/app-mvp-build.svg)](https://hits.dwyl.com/dwyl/app-mvp)
\ No newline at end of file
diff --git a/assets/js/app.js b/assets/js/app.js
index 9797d5b1..eaa495f1 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -14,13 +14,13 @@ window.addEventListener("phx:page-loading-stop", info => topbar.hide())
window.addEventListener("phx:highlight", (e) => {
document.querySelectorAll("[data-highlight]").forEach(el => {
if(el.id == e.detail.id) {
- liveSocket.execJS(el, el.getAttribute("data-highlight"))
+ live_socket.execJS(el, el.getAttribute("data-highlight"))
}
})
})
-// Item id of the destination in the DOM
-let itemId_to;
+// Item ID of the destination during drag over in the DOM
+let item_id_destination;
let Hooks = {}
Hooks.Items = {
@@ -33,31 +33,31 @@ Hooks.Items = {
})
this.el.addEventListener("remove-highlight", e => {
- hook.pushEventTo("#items", "removeHighlight", {id: e.detail.id})
+ hook.pushEventTo("#items", "remove_highlight", {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("dragover_item", e => {
+ // console.log("dragover_item", e.detail)
+ const current_item_id = e.detail.current_item.id
+ const selected_item_id = e.detail.selected_item_id
+ if( current_item_id != selected_item_id) {
+ hook.pushEventTo("#items", "dragover_item", {current_item_id: current_item_id, selected_item_id: selected_item_id})
+ item_id_destination = e.detail.current_item.dataset.id
}
})
- this.el.addEventListener("update-indexes", e => {
+ 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)
+ 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) {
+ if(item_id && item_id_destination && item_id != item_id_destination) {
hook.pushEventTo("#items", "update_list_seq",
{seq: list_ids})
}
- itemId_to = null;
+ item_id_destination = null;
})
}
}
@@ -78,33 +78,33 @@ function get_list_item_cids() {
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"))
+ live_socket.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 selected_item = document.querySelector(`#${e.detail.selected_item_id}`)
+ const current_item = document.querySelector(`#${e.detail.current_item_id}`)
const items = document.querySelector('#items')
- const listItems = [...document.querySelectorAll('.item')]
+ const list_items = [...document.querySelectorAll('.item')]
- if(listItems.indexOf(selectedItem) < listItems.indexOf(currentItem)){
- items.insertBefore(selectedItem, currentItem.nextSibling)
+ if(list_items.indexOf(selected_item) < list_items.indexOf(current_item)){
+ items.insertBefore(selected_item, current_item.nextSibling)
}
- if(listItems.indexOf(selectedItem) > listItems.indexOf(currentItem)){
- items.insertBefore(selectedItem, currentItem)
+ if(list_items.indexOf(selected_item) > list_items.indexOf(current_item)){
+ items.insertBefore(selected_item, current_item)
}
})
-// liveSocket related setup:
+// live_socket related setup:
-let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+let csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content")
-let liveSocket = new LiveSocket("/live", Socket, {
+let live_socket = new LiveSocket("/live", Socket, {
hooks: Hooks,
dom:{
onBeforeElUpdated(from, to) {
@@ -114,17 +114,17 @@ let liveSocket = new LiveSocket("/live", Socket, {
}
},
params: {
- _csrf_token: csrfToken,
+ _csrf_token: csrf_token,
hours_offset_fromUTC: -new Date().getTimezoneOffset()/60
}
})
// connect if there are any LiveViews on the page
-liveSocket.connect()
+live_socket.connect()
-// expose liveSocket on window for web console debug logs and latency simulation:
-// >> liveSocket.enableDebug()
-// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
-// >> liveSocket.disableLatencySim()
-window.liveSocket = liveSocket
+// expose live_socket on window for web console debug logs and latency simulation:
+// >> live_socket.enableDebug()
+// >> live_socket.enableLatencySim(1000) // enabled for duration of browser session
+// >> live_socket.disableLatencySim()
+window.live_socket = live_socket
diff --git a/lib/app/item.ex b/lib/app/item.ex
index 43a1e742..fc8d8590 100644
--- a/lib/app/item.ex
+++ b/lib/app/item.ex
@@ -38,7 +38,13 @@ defmodule App.Item do
item
|> cast(attrs, [:person_id, :status, :text])
|> validate_required([:person_id])
- |> App.Cid.put_cid()
+ end
+
+ # Update an item without changing the cid ref: #418
+ def changeset_update(item, attrs) do
+ item
+ |> cast(attrs, [:cid, :person_id, :status, :text])
+ |> validate_required([:cid, :text, :person_id])
end
@doc """
@@ -137,13 +143,13 @@ defmodule App.Item do
[%Item{}, ...]
"""
- def list_items do
+ def get_items do
Item
|> where([i], is_nil(i.status) or i.status != 6)
|> Repo.all()
end
- def list_person_items(person_id) do
+ def get_person_items(person_id) do
Item
|> where(person_id: ^person_id)
|> Repo.all()
@@ -164,7 +170,7 @@ defmodule App.Item do
"""
def update_item(%Item{} = item, attrs) do
item
- |> Item.changeset(attrs)
+ |> Item.changeset_update(attrs)
|> PaperTrail.update(originator: %{id: Map.get(attrs, :person_id, 0)})
end
@@ -186,7 +192,7 @@ defmodule App.Item do
def delete_item(id) do
get_item!(id)
- |> Item.changeset(%{status: 6})
+ |> Item.changeset_update(%{status: 6})
|> Repo.update()
end
@@ -201,21 +207,28 @@ defmodule App.Item do
# 👩💻 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.
+ `items_with_timers/2` Returns a List of items with the latest associated timers.
+ The result set is ordered by the `list.seq`.
+ Accepts an optional second parameter `list_cid` which is the unique ID of the `list`
+ to retrieve `items` for.
## Examples
iex> items_with_timers()
[
- %{text: "hello", person_id: 1, status: 2, start: 2022-07-14 09:35:18},
- %{text: "world", person_id: 2, status: 7, start: 2022-07-15 04:20:42}
+ %{text: "hello", person_id: 0, status: 2, start: 2022-07-14 09:35:18},
+ %{text: "world", person_id: 0, status: 7, start: 2022-07-15 04:20:42}
]
"""
- #
- 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)
+ def items_with_timers(person_id \\ 0, list_cid \\ nil) do
+ seq =
+ if is_nil(list_cid) do
+ App.List.get_all_list_for_person(person_id)
+ |> App.List.get_list_seq()
+ else
+ App.List.get_list_by_cid!(list_cid)
+ |> App.List.get_list_seq()
+ end
sql = """
SELECT i.id, i.cid, i.text, i.status, i.person_id, i.updated_at,
@@ -233,7 +246,7 @@ defmodule App.Item do
|> map_columns_to_values()
items_tags =
- list_person_items(person_id)
+ get_person_items(person_id)
|> Enum.reduce(%{}, fn i, acc -> Map.put(acc, i.id, i) end)
accumulate_item_timers(values, seq)
@@ -378,7 +391,7 @@ defmodule App.Item do
This will not be needed once all records are transitioned.
"""
def update_all_items_cid do
- items = list_items()
+ items = get_items()
Enum.each(items, fn i ->
# coveralls-ignore-start
diff --git a/lib/app/list.ex b/lib/app/list.ex
index a4d7c993..74d351b6 100644
--- a/lib/app/list.ex
+++ b/lib/app/list.ex
@@ -18,13 +18,20 @@ defmodule App.List do
end
@doc false
- def changeset(list, attrs) do
+ def changeset(list, attrs \\ %{}) do
list
|> cast(attrs, [:name, :person_id, :seq, :sort, :status])
|> validate_required([:name, :person_id])
|> App.Cid.put_cid()
end
+ # Update an list without changing the cid ref: #418
+ def changeset_update(list, attrs \\ %{}) do
+ list
+ |> cast(attrs, [:name, :person_id, :seq, :sort, :status])
+ |> validate_required([:cid, :name, :person_id])
+ end
+
@doc """
`create_list/1` creates an `list`.
@@ -62,6 +69,16 @@ defmodule App.List do
|> Repo.get!(id)
end
+ @doc """
+ `delete_list/1` "soft" deletes a list
+ so it can easily be restored in the event of mistaken deletion.
+ """
+ def delete_list(id) do
+ get_list!(id)
+ |> changeset_update(%{status: 6})
+ |> Repo.update()
+ end
+
@doc """
`get_list_by_cid!/1` gets the `list` record by its' `cid`.
@@ -96,7 +113,7 @@ defmodule App.List do
"""
def update_list(%List{} = list, attrs) do
list
- |> List.changeset(attrs)
+ |> List.changeset_update(attrs)
|> PaperTrail.update()
end
@@ -106,6 +123,8 @@ defmodule App.List do
def get_lists_for_person(person_id) do
List
|> where(person_id: ^person_id)
+ |> where([l], l.status != 6)
+ |> order_by(asc: :name)
|> Repo.all()
end
@@ -146,6 +165,17 @@ defmodule App.List do
end
end
+ @doc """
+ `update_list_seq/3` update the `list.seq` for the `list.cid` for the `person_id`.
+ """
+ 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
+
+ @doc """
+ `add_item_to_list/3` adds the `item.cid` to the `list.cid` for the given `person_id`.
+ """
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)
@@ -153,11 +183,33 @@ defmodule App.List do
update_list(list, %{seq: seq, person_id: person_id})
end
- def update_list_seq(list_cid, person_id, seq) do
+ @doc """
+ `remove_item_from_list/3` update the `list.seq` for the `list.cid` for the `person_id`.
+ """
+ def remove_item_from_list(item_cid, list_cid, person_id) do
list = get_list_by_cid!(list_cid)
+ # get existing list.seq
+ seq =
+ get_list_seq(list)
+ # remove the item_cid from the list.seq:
+ |> Useful.remove_item_from_list(item_cid)
+ |> Enum.join(",")
+
update_list(list, %{seq: seq, person_id: person_id})
end
+ @doc """
+ `move_item_from_lista_to_listb/4` moves the `item_cid`
+ from `lista_cid` to `listb_cid` (removes from `lista`) for `person_id`.
+ "From A to B".
+ """
+ def move_item_from_lista_to_listb(item_cid, lista_cid, listb_cid, person_id) do
+ # Remove from List A:
+ remove_item_from_list(item_cid, lista_cid, person_id)
+ # Add to List B:
+ add_item_to_list(item_cid, listb_cid, 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:
@@ -174,6 +226,13 @@ defmodule App.List do
tuple
end
+ @doc """
+ `get_list_seq/1` receives a `%List{}` `Map`/`Struct`
+ and returns a `List` of `item` `cids`
+ that can easily be used to lookup which `items` are on a given `list`.
+ e.g: ["F4VbyA5NNSxNvVwAAAWi", "A8y3Fk4ht", "RJX0VSn", "etc"]
+ OR if the `list` does not yet have an `items`, returns an *empty* `List`: []
+ """
def get_list_seq(list) do
if is_nil(list.seq) do
[]
diff --git a/lib/app_web.ex b/lib/app_web.ex
index 36833c05..a9119adb 100644
--- a/lib/app_web.ex
+++ b/lib/app_web.ex
@@ -34,7 +34,9 @@ defmodule AppWeb do
quote do
use Phoenix.View,
root: "lib/app_web/templates",
- namespace: AppWeb
+ namespace: AppWeb,
+ # elixirforum.com/t/html-heex-file-for-phoenix-component-template/42516/4
+ pattern: "**/*"
# Import convenience functions from controllers
import Phoenix.Controller,
diff --git a/lib/app_web/controllers/list_controller.ex b/lib/app_web/controllers/list_controller.ex
new file mode 100644
index 00000000..48389105
--- /dev/null
+++ b/lib/app_web/controllers/list_controller.ex
@@ -0,0 +1,93 @@
+defmodule AppWeb.ListController do
+ use AppWeb, :controller
+ alias App.List
+ plug :permission_list when action in [:edit, :update, :delete]
+
+ def index(conn, _params) do
+ person_id = conn.assigns[:person][:id] || 0
+ lists = List.get_lists_for_person(person_id)
+
+ render(conn, "index.html", lists: lists, custom_list: false)
+ end
+
+ def new(conn, _params) do
+ changeset = List.changeset(%List{})
+ render(conn, "new.html", changeset: changeset)
+ end
+
+ defp downcase_name(list_params) do
+ list_name = Map.get(list_params, "name") || nil
+
+ if list_name != nil do
+ Map.put(list_params, "name", String.downcase(list_name))
+ else
+ list_params
+ end
+ end
+
+ def create(conn, %{"list" => list_params}) do
+ person_id = conn.assigns[:person][:id] || 0
+
+ list_params =
+ downcase_name(list_params)
+ |> Map.put("person_id", person_id)
+ |> Map.put("status", 2)
+
+ case List.create_list(list_params) do
+ {:ok, _list} ->
+ conn
+ |> put_flash(:info, "List created successfully.")
+ |> redirect(to: Routes.list_path(conn, :index))
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, "new.html", changeset: changeset)
+ end
+ end
+
+ # TODO: need to ensure we are checking for the person_id before editing
+ # Should return 404 if person_id is not valid for this list.
+ def edit(conn, %{"id" => id}) do
+ list = List.get_list!(id)
+ changeset = List.changeset(list)
+ render(conn, "edit.html", list: list, changeset: changeset)
+ end
+
+ def update(conn, %{"id" => id, "list" => list_params}) do
+ list = List.get_list!(id)
+ list_params = downcase_name(list_params)
+
+ case List.update_list(list, list_params) do
+ {:ok, _list} ->
+ conn
+ |> put_flash(:info, "List updated successfully.")
+ |> redirect(to: Routes.list_path(conn, :index))
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, "edit.html", list: list, changeset: changeset)
+ end
+ end
+
+ # TODO: MUST check person_id matches before allowing "delete"!
+ def delete(conn, %{"id" => id}) do
+ list = List.get_list!(id)
+ {:ok, _list} = List.delete_list(list.id)
+
+ conn
+ |> put_flash(:info, "list deleted successfully.")
+ |> redirect(to: Routes.list_path(conn, :index))
+ end
+
+ defp permission_list(conn, _opts) do
+ list = List.get_list!(conn.params["id"])
+ person_id = conn.assigns[:person][:id] || 0
+
+ if list.person_id == person_id && list.name != "all" do
+ conn
+ else
+ conn
+ |> put_flash(:info, "You can't access that page")
+ |> redirect(to: "/lists")
+ |> halt()
+ end
+ end
+end
diff --git a/lib/app_web/controllers/tag_controller.ex b/lib/app_web/controllers/tag_controller.ex
index c8b525e8..0fa43d67 100644
--- a/lib/app_web/controllers/tag_controller.ex
+++ b/lib/app_web/controllers/tag_controller.ex
@@ -7,7 +7,11 @@ defmodule AppWeb.TagController do
person_id = conn.assigns[:person][:id] || 0
tags = Tag.list_person_tags(person_id)
- render(conn, "index.html", tags: tags)
+ render(conn, "index.html",
+ tags: tags,
+ lists: App.List.get_lists_for_person(person_id),
+ custom_list: false
+ )
end
def new(conn, _params) do
diff --git a/lib/app_web/live/app_live.ex b/lib/app_web/live/app_live.ex
index c6c884ef..ef63cf6c 100644
--- a/lib/app_web/live/app_live.ex
+++ b/lib/app_web/live/app_live.ex
@@ -13,19 +13,41 @@ defmodule AppWeb.AppLive do
@stats_topic "stats"
defp get_list_cid(assigns), do: assigns[:list_cid]
+ defp get_list_name(assigns), do: assigns[:list_name]
+
+ defp list_cid_from_url_params(params) do
+ if Map.has_key?(params, "list_cid"), do: Map.get(params, "list_cid", nil)
+ end
@impl true
- def mount(_params, _session, socket) do
+ def mount(params, _session, socket) do
# subscribe to the channel
if connected?(socket), do: AppWeb.Endpoint.subscribe(@topic)
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)
+
+ custom_list = list_cid_from_url_params(params)
+
+ list_cid =
+ if custom_list == nil do
+ # 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)
+
+ # return the "all" list cid
+ all_list.cid
+ else
+ custom_list
+ end
+
+ lists = App.List.get_lists_for_person(person_id)
+ list = Enum.find(lists, fn list -> list.cid == list_cid end)
+
+ # Assigns
+ items = Item.items_with_timers(person_id, list_cid)
tags = Tag.list_person_tags(person_id)
selected_tags = []
draft_item = Item.get_draft_item(person_id)
@@ -37,7 +59,10 @@ defmodule AppWeb.AppLive do
editing: nil,
filter: "active",
filter_tag: nil,
- list_cid: all_list.cid,
+ custom_list: custom_list,
+ list_cid: list_cid,
+ list_name: list.name,
+ lists: lists,
tags: tags,
selected_tags: selected_tags,
text_value: draft_item.text || "",
@@ -60,7 +85,7 @@ defmodule AppWeb.AppLive do
def handle_event("create", %{"text" => text}, socket) do
person_id = Person.get_person_id(socket.assigns)
- {:ok, %{model: _item}} =
+ {:ok, %{model: item}} =
Item.create_item_with_tags(%{
text: text,
person_id: person_id,
@@ -68,8 +93,13 @@ defmodule AppWeb.AppLive do
tags: socket.assigns.selected_tags
})
- # Add this newly created `item` to the "All" list:
- # App.ListItem.add_item_to_all_list(item)
+ # Add this newly created `item` to the list current list:
+ list_cid = get_list_cid(socket.assigns)
+ list_name = get_list_name(socket.assigns)
+
+ if list_name !== "all" do
+ App.List.add_item_to_list(item.cid, list_cid, person_id)
+ end
draft = Item.get_draft_item(person_id)
Item.update_draft(draft, %{text: ""})
@@ -273,17 +303,17 @@ defmodule AppWeb.AppLive do
end
@impl true
- def handle_event("removeHighlight", %{"id" => id}, socket) do
+ def handle_event("remove_highlight", %{"id" => id}, socket) do
AppWeb.Endpoint.broadcast(@topic, "move_items", {:drop_item, id})
{:noreply, socket}
end
@impl true
def handle_event(
- "dragoverItem",
+ "dragover_item",
%{
- "currentItemId" => current_item_id,
- "selectedItemId" => selected_item_id
+ "current_item_id" => current_item_id,
+ "selected_item_id" => selected_item_id
},
socket
) do
@@ -342,11 +372,12 @@ defmodule AppWeb.AppLive do
@impl true
def handle_info(%Broadcast{event: "update", payload: payload}, socket) do
person_id = Person.get_person_id(socket.assigns)
- items = Item.items_with_timers(person_id)
- isEditingItem = socket.assigns.editing
+ list_cid = get_list_cid(socket.assigns)
+ items = Item.items_with_timers(person_id, list_cid)
+ is_editing_item = socket.assigns.editing
# If the item is being edited, we update the timer list of the item being edited.
- if isEditingItem do
+ if is_editing_item do
case payload do
{:start, item_id} ->
timers_list_changeset = Timer.list_timers_changesets(item_id)
diff --git a/lib/app_web/live/app_live.html.heex b/lib/app_web/live/app_live.html.heex
index 45a9d2a2..fec2c4f7 100644
--- a/lib/app_web/live/app_live.html.heex
+++ b/lib/app_web/live/app_live.html.heex
@@ -142,7 +142,7 @@
-