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

Save brainstormings in local storage #520

Merged
merged 22 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
!*.heex
!*.ex
!*.exs
!*.json
!*.js
!*.jsx
!*.ts
!*.tsx
!*.scss
!*.css
!*.html
43 changes: 41 additions & 2 deletions assets/js/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Modal, Tooltip } from "bootstrap"
import Sortable from 'sortablejs';
import { setIdeaLabelBackgroundColor } from "./label"

import { setIdeaLabelBackgroundColor } from "./label";
// activate all tooltips:
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl));

const sortBrainstormingsByLastAccessedAt = (brainstormings, sliceMax = 10) => {
return Object.values(brainstormings).sort((a, b) => new Date(b.last_accessed_at) - new Date(a.last_accessed_at)).slice(0, sliceMax)
}

// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
Expand Down Expand Up @@ -131,6 +134,42 @@ Hooks.SetIdeaLabelBackgroundColor = {
}
};

Hooks.TransferLocalStorageBrainstormings = {
mounted() {
const recentBrainstormings = JSON.parse(localStorage.getItem('brainstormings') || '{}');
const lastSortedBrainstormings = sortBrainstormingsByLastAccessedAt(recentBrainstormings, 5)
this.pushEventTo(this.el, "brainstormings_from_local_storage", lastSortedBrainstormings)
}
}

Hooks.StoreRecentBrainstorming = {
mounted() {
const brainstormingId = this.el.dataset.id;
const recentBrainstormings = JSON.parse(localStorage.getItem('brainstormings') || '{}');

recentBrainstormings[brainstormingId] = {
id: brainstormingId,
admin_url_id: this.el.dataset.adminUrlId || recentBrainstormings?.brainstormingId?.admin_url_id,
name: this.el.dataset.name,
last_accessed_at: this.el.dataset.lastAccessedAt
}
localStorage.setItem('brainstormings', JSON.stringify(recentBrainstormings));
const lastSortedBrainstormings = sortBrainstormingsByLastAccessedAt(recentBrainstormings)
this.pushEventTo(this.el,"brainstormings_from_local_storage", lastSortedBrainstormings)
}
};

Hooks.RemoveMissingBrainstorming = {
mounted() {
const missingId = this.el.dataset.brainstormingId;
if (missingId) {
const recentBrainstormings = JSON.parse(localStorage.getItem('brainstormings') || '{}');
delete recentBrainstormings[missingId]
localStorage.setItem('brainstormings', JSON.stringify(recentBrainstormings));
}
}
};

// The brainstorming secret from the url ("#123") is added as well to the socket. The secret is not available on the server side by default.
let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks, params: { _csrf_token: csrfToken, adminSecret: window.location.hash.substring(1) }
Expand Down
6 changes: 6 additions & 0 deletions assets/scss/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ a .bi {

/* LiveView specific classes for your customizations */

// similar to .phx-connected but also works when javascript is disabled
div[data-phx-session] {
height: 100%;
width: 100%;
}

.phx-click-loading {
opacity: 0.5;
transition: opacity 1s ease-out;
Expand Down
10 changes: 8 additions & 2 deletions lib/mindwendel/brainstormings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,22 @@ defmodule Mindwendel.Brainstormings do
[%Brainstorming{}, ...]

"""
def list_brainstormings_for(user_id, limit \\ 3) do
def list_brainstormings_for(user_id, limit \\ 3)

def list_brainstormings_for(user_id, limit) when user_id != nil do
JannikStreek marked this conversation as resolved.
Show resolved Hide resolved
Repo.all(
from brainstorming in Brainstorming,
join: users in assoc(brainstorming, :users),
where: users.id == ^user_id,
order_by: [desc: brainstorming.inserted_at],
order_by: [desc: brainstorming.last_accessed_at],
limit: ^limit
)
end

def list_brainstormings_for(nil, _) do
[]
end

@doc """
Returns the list of brainstormings.

Expand Down
2 changes: 1 addition & 1 deletion lib/mindwendel/likes.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Mindwendel.Likes do
@moduledoc """
The Brainstormings context.
The Likes context.
"""

import Ecto.Query, warn: false
Expand Down
70 changes: 70 additions & 0 deletions lib/mindwendel/local_storage.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
defmodule Mindwendel.LocalStorage do
alias Mindwendel.Permissions

@moduledoc """
The LocalStorage context. It includes helpers to handle local storage data from clients.
"""

# This function is used to merge the brainstormings from local storage and the session.
# It returns a list of brainstormings sorted by the last_accessed_at field.
def brainstormings_from_local_storage_and_session(
brainstormings_from_local_storage,
brainstormings_from_session,
user
) do
(brainstormings_from_local_storage(brainstormings_from_local_storage) ++
brainstormings_from_session(brainstormings_from_session, user))
|> Enum.uniq_by(& &1["id"])
|> Enum.sort(&(&1["last_accessed_at"] > &2["last_accessed_at"]))
JannikStreek marked this conversation as resolved.
Show resolved Hide resolved
end

defp brainstormings_from_local_storage(brainstormings_stored)
when is_list(brainstormings_stored) do
brainstormings_stored
|> Enum.map(fn e ->
Map.put(e, "last_accessed_at", format_iso8601(e["last_accessed_at"]))
end)
|> Enum.filter(&valid_stored_brainstorming?/1)
end

defp brainstormings_from_local_storage(_) do
[]
end

defp brainstormings_from_session(brainstormings, user) when is_list(brainstormings) do
Enum.map(brainstormings, fn brainstorming ->
%{
"last_accessed_at" => brainstorming.last_accessed_at,
"name" => brainstorming.name,
"id" => brainstorming.id,
"admin_url_id" =>
if(Permissions.has_moderating_permission(brainstorming, user),
do: brainstorming.admin_url_id,
else: nil
)
}
end)
end

defp brainstormings_from_session(_, _) do
[]
end

defp valid_stored_brainstorming?(brainstorming) do
case Ecto.UUID.cast(brainstorming["id"]) do
{:ok, _} -> brainstorming["last_accessed_at"] && brainstorming["name"]
JannikStreek marked this conversation as resolved.
Show resolved Hide resolved
:error -> false
end
end

defp format_iso8601(iso8601) when iso8601 != nil do
case DateTime.from_iso8601(iso8601) do
{:ok, date_time, _} -> date_time
{:error, _} -> nil
end
end

defp format_iso8601(_) do
nil
end
end
12 changes: 12 additions & 0 deletions lib/mindwendel/permissions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Mindwendel.Permissions do
@moduledoc """
The Permissions context.
"""
def has_moderating_permission(brainstorming, current_user) when current_user != nil do
JannikStreek marked this conversation as resolved.
Show resolved Hide resolved
Enum.member?(current_user.moderated_brainstormings |> Enum.map(& &1.id), brainstorming.id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote a small essay on this, but this is the "single pipe" which is discouraged: rrrene/credo#221

end

def has_moderating_permission(_, _) do
false
end
end
2 changes: 1 addition & 1 deletion lib/mindwendel_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule MindwendelWeb do
quote do
use Phoenix.Controller,
formats: [:html, :json],
layouts: [html: MindwendelWeb.Layouts]
layouts: [html: {MindwendelWeb.Layouts, :app_static}]

import Plug.Conn
use Gettext, backend: MindwendelWeb.Gettext
Expand Down
5 changes: 2 additions & 3 deletions lib/mindwendel_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ defmodule MindwendelWeb.Layouts do
Brainstormings.list_brainstormings_for(user.id, limit)
end

def admin_route(conn) do
route_scope = conn.request_path |> String.split("/", trim: true) |> List.first()
route_scope == "admin"
def admin_view(current_view) do
current_view == MindwendelWeb.Admin.BrainstormingLive.Edit
end
end
19 changes: 0 additions & 19 deletions lib/mindwendel_web/controllers/static_page_controller.ex
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
defmodule MindwendelWeb.StaticPageController do
use MindwendelWeb, :controller
alias Mindwendel.Brainstormings
alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.FeatureFlag

plug :put_root_layout, {MindwendelWeb.Layouts, :static_page}

def home(conn, _params) do
current_user =
conn
|> Mindwendel.Services.SessionService.get_current_user_id()
|> Mindwendel.Accounts.get_user()

form =
%Brainstorming{}
|> Brainstormings.change_brainstorming(%{})
|> Phoenix.Component.to_form()

render(conn, "home.html",
current_user: current_user,
form: form
)
end

def legal(conn, _params) do
if FeatureFlag.enabled?(:feature_privacy_imprint_enabled) do
render(conn, "legal.html")
Expand Down
12 changes: 0 additions & 12 deletions lib/mindwendel_web/controllers/static_page_html.ex
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
defmodule MindwendelWeb.StaticPageHTML do
use MindwendelWeb, :html
alias Mindwendel.Brainstormings

embed_templates "static_page_html/*"

def list_brainstormings_for(user) do
Brainstormings.list_brainstormings_for(user.id)
end

def brainstormings_available_until() do
Timex.Duration.from_days(
Application.fetch_env!(:mindwendel, :options)[:feature_brainstorming_removal_after_days]
)
|> Timex.format_duration(:humanized)
end
end
1 change: 1 addition & 0 deletions lib/mindwendel_web/live/admin/brainstorming_live/edit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ defmodule MindwendelWeb.Admin.BrainstormingLive.Edit do
{
:ok,
socket
|> assign(:current_view, socket.view)
|> assign(:page_title, "Admin")
|> assign(:brainstorming, brainstorming)
|> assign(:form, to_form(changeset))
Expand Down
19 changes: 18 additions & 1 deletion lib/mindwendel_web/live/brainstorming_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
alias Mindwendel.Ideas
alias Mindwendel.Brainstormings.Idea
alias Mindwendel.Brainstormings.Lane
alias Mindwendel.LocalStorage

@impl true
def mount(%{"id" => id}, session, socket) do
Expand All @@ -33,6 +34,8 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
{
:ok,
socket
|> assign(:brainstormings_stored, [])
|> assign(:current_view, socket.view)
|> assign(:brainstorming, brainstorming)
|> assign(:lanes, lanes)
|> assign(:current_user, current_user)
Expand All @@ -42,8 +45,9 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
{:error, _} ->
{:ok,
socket
|> put_flash(:missing_brainstorming_id, id)
|> put_flash(:error, gettext("Brainstorming not found"))
|> push_navigate(to: "/")}
|> redirect(to: "/")}
end
end

Expand All @@ -59,6 +63,19 @@ defmodule MindwendelWeb.BrainstormingLive.Show do
mount(%{"id" => id}, session, socket)
end

@impl true
def handle_event("brainstormings_from_local_storage", brainstormings_stored, socket) do
# Brainstormings are used from session data and local storage. Session data can be removed later and is only used for a transition period.
valid_stored_brainstormings =
LocalStorage.brainstormings_from_local_storage_and_session(
brainstormings_stored,
Brainstormings.list_brainstormings_for(socket.assigns.current_user.id),
socket.assigns.current_user
)

{:noreply, assign(socket, :brainstormings_stored, valid_stored_brainstormings)}
end

@impl true
def handle_params(
params,
Expand Down
13 changes: 12 additions & 1 deletion lib/mindwendel_web/live/brainstorming_live/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@
</div>
<% end %>

<div id="brainstorming-details">
<div
id="brainstorming-details"
data-id={@brainstorming.id}
data-name={@brainstorming.name}
data-last-accessed-at={@brainstorming.last_accessed_at}
data-admin-url-id={
if has_moderating_permission(@brainstorming, @current_user),
do: @brainstorming.admin_url_id,
else: nil
}
phx-hook="StoreRecentBrainstorming"
>
<div class="content">
<div class="row">
<div class="col-sm-12 col-md-12 col-xl-6">
Expand Down
10 changes: 9 additions & 1 deletion lib/mindwendel_web/live/live_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ defmodule MindwendelWeb.LiveHelpers do

alias Mindwendel.Brainstormings.Brainstorming
alias Mindwendel.FeatureFlag
alias Mindwendel.Permissions

def has_move_permission(brainstorming, current_user) do
brainstorming.option_allow_manual_ordering or
has_moderating_permission(brainstorming, current_user)
end

def has_moderating_permission(brainstorming, current_user) do
Enum.member?(current_user.moderated_brainstormings |> Enum.map(& &1.id), brainstorming.id)
Permissions.has_moderating_permission(brainstorming, current_user)
end

def has_ownership(record, current_user) do
Expand All @@ -36,6 +37,13 @@ defmodule MindwendelWeb.LiveHelpers do
Brainstorming.brainstorming_available_until(brainstorming)
end

def brainstormings_available_until() do
Timex.Duration.from_days(
Application.fetch_env!(:mindwendel, :options)[:feature_brainstorming_removal_after_days]
)
|> Timex.format_duration(:humanized)
end

def show_idea_file_upload? do
FeatureFlag.enabled?(:feature_file_upload)
end
Expand Down
Loading
Loading