- <%= for item <- Map.get(section, :items, []) do %>
-
- <% end %>
-
-
- <% end %>
- <%= if Enum.empty?(Map.get(@sidebar_data, :sections, [])) do %>
-
-
<%= translated(Map.get(@sidebar_data, :empty_message, "No items")) %>
-
- <% end %>
- """
- end
-
- defp render_media_sidebar(assigns) do
- ~H"""
- <%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %>
-
- <%= for item <- Map.get(@sidebar_data, :items, []) do %>
-
- <% end %>
-
- <% else %>
-
-
<%= translated(Map.get(@sidebar_data, :empty_message, "No items")) %>
-
- <% end %>
- """
- end
-
- defp render_entity_sidebar(assigns) do
- ~H"""
- <%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %>
-
- <%= for item <- Map.get(@sidebar_data, :items, []) do %>
-
- <% end %>
-
- <% else %>
-
-
<%= translated(Map.get(@sidebar_data, :empty_message, "No items")) %>
-
- <% end %>
- """
- end
-
- defp render_nav_sidebar(assigns) do
- ~H"""
-
- <%= for item <- Map.get(@sidebar_data, :items, []) do %>
-
- <% end %>
-
- """
- end
-
- defp render_default_sidebar(assigns) do
- ~H"""
- <%= for section <- Map.get(@sidebar_data, :sections, []) do %>
-
-
- <%= translated(section.title) %>
-
-
- <%= for item <- Map.get(section, :items, []) do %>
-
<%= item.title || "" %>
- <% end %>
-
-
- <% end %>
- """
- end
-
defp render_panel_body(assigns) do
case assigns.workbench.panel.active_tab do
:tasks -> render_task_entries(assigns)
@@ -1274,20 +875,6 @@ defmodule BDS.Desktop.ShellLive do
max(4, ((entry.count || 0) / max_count) * 100)
end
- defp format_sidebar_timestamp(nil), do: ""
-
- defp format_sidebar_timestamp(timestamp) do
- timestamp
- |> DateTime.from_unix!(:millisecond)
- |> Calendar.strftime("%x")
- end
-
- defp image_media?(item), do: String.starts_with?(to_string(item.mime_type || ""), "image/")
-
- defp media_thumbnail_class(item) do
- if image_media?(item), do: "media-thumbnail has-image", else: "media-thumbnail"
- end
-
defp current_tab(%{active_tab: nil}), do: nil
defp current_tab(%{tabs: tabs, active_tab: {type, id}}) do
@@ -1295,163 +882,7 @@ defmodule BDS.Desktop.ShellLive do
end
defp assign_post_editor(socket) do
- assigns = Map.put(socket.assigns, :project_metadata, ShellOverlayComponents.project_metadata(socket.assigns.projects.active_project_id))
- assign(socket, :post_editor, PostEditor.build(assigns))
- end
-
- defp update_post_editor(socket, params) do
- case socket.assigns.current_tab do
- %{type: :post, id: post_id} ->
- case Repo.get(Post, post_id) do
- nil ->
- socket
-
- %Post{} = post ->
- metadata = ShellOverlayComponents.project_metadata(post.project_id)
- canonical_language = PostEditor.normalize_language(post.language, metadata.main_language || "en")
- current_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
- requested_language = PostEditor.normalize_language(Map.get(params, "language"), current_language)
-
- next_language =
- if current_language == canonical_language do
- requested_language
- else
- current_language
- end
-
- draft = PostEditor.normalize_params(params, current_language, next_language)
- workbench = Workbench.mark_dirty(socket.assigns.workbench, :post, post_id)
-
- socket
- |> assign(:workbench, workbench)
- |> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, next_language, draft))
- |> assign(:post_editor_active_languages, Map.put(socket.assigns.post_editor_active_languages, post_id, next_language))
- |> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, :dirty))
- |> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: draft["title"], subtitle: Atom.to_string(post.status || :draft)}))
- |> maybe_drop_old_language_draft(post_id, current_language, next_language)
- |> reload_shell(workbench)
- end
-
- _other ->
- socket
- end
- end
-
- defp maybe_drop_old_language_draft(socket, _post_id, current_language, next_language) when current_language == next_language,
- do: socket
-
- defp maybe_drop_old_language_draft(socket, post_id, current_language, _next_language) do
- assign(socket, :post_editor_drafts, delete_nested_map(socket.assigns.post_editor_drafts, post_id, current_language))
- end
-
- defp persist_post_editor(socket, post_id, action) do
- case Repo.get(Post, post_id) do
- nil ->
- socket
-
- %Post{} = post ->
- metadata = ShellOverlayComponents.project_metadata(post.project_id)
- canonical_language = PostEditor.normalize_language(post.language, metadata.main_language || "en")
- active_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
- draft = PostEditor.current_draft(socket.assigns, post, metadata, active_language)
-
- result = PostEditor.persist(post, draft, active_language, metadata, action)
-
- case result do
- {:ok, record} ->
- workbench = Workbench.clear_dirty(socket.assigns.workbench, :post, post_id)
- normalized_form = PostEditor.persisted_form(Repo.get!(Post, post_id), metadata, active_language)
-
- socket
- |> assign(:workbench, workbench)
- |> assign(:post_editor_drafts, put_nested_map(socket.assigns.post_editor_drafts, post_id, active_language, normalized_form))
- |> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, PostEditor.save_state_for_action(action)))
- |> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: PostEditor.record_title(record, Repo.get!(Post, post_id)), subtitle: Atom.to_string(PostEditor.record_status(record))}))
- |> reload_shell(workbench)
-
- {:error, reason} ->
- socket
- |> append_output_entry(translated("Post"), inspect(reason), nil, "error")
- |> reload_shell(socket.assigns.workbench)
- end
- end
- end
-
- defp discard_post_editor(socket, post_id) do
- case Repo.get(Post, post_id) do
- nil ->
- socket
-
- %Post{} = post ->
- metadata = ShellOverlayComponents.project_metadata(post.project_id)
- canonical_language = PostEditor.normalize_language(post.language, metadata.main_language || "en")
- active_language = Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language)
- restored_result = PostEditor.discard(post, active_language, metadata)
-
- case restored_result do
- {:ok, restored_post} ->
- workbench = Workbench.clear_dirty(socket.assigns.workbench, :post, post_id)
-
- socket
- |> assign(:workbench, workbench)
- |> assign(:post_editor_drafts, delete_nested_map(socket.assigns.post_editor_drafts, post_id, active_language))
- |> assign(:post_editor_save_states, Map.put(socket.assigns.post_editor_save_states, post_id, :discarded))
- |> assign(:tab_meta, Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: restored_post.title || restored_post.slug || restored_post.id, subtitle: Atom.to_string(restored_post.status || :draft)}))
- |> reload_shell(workbench)
-
- {:error, reason} ->
- socket
- |> append_output_entry(translated("Post"), inspect(reason), nil, "error")
- |> reload_shell(socket.assigns.workbench)
- end
- end
- end
-
- defp delete_post_editor(socket, post_id) do
- case Posts.delete_post(post_id) do
- {:ok, :deleted} ->
- workbench = Workbench.close_tab(socket.assigns.workbench, :post, post_id)
-
- socket
- |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:post, post_id}))
- |> assign(:post_editor_drafts, Map.delete(socket.assigns.post_editor_drafts, post_id))
- |> assign(:post_editor_active_languages, Map.delete(socket.assigns.post_editor_active_languages, post_id))
- |> assign(:post_editor_modes, Map.delete(socket.assigns.post_editor_modes, post_id))
- |> assign(:post_editor_expanded, Map.delete(socket.assigns.post_editor_expanded, post_id))
- |> assign(:post_editor_save_states, Map.delete(socket.assigns.post_editor_save_states, post_id))
- |> reload_shell(workbench)
-
- {:error, reason} ->
- socket
- |> append_output_entry(translated("Post"), inspect(reason), nil, "error")
- |> reload_shell(socket.assigns.workbench)
- end
- end
-
- defp update_post_editor_expanded(socket, post_id, updater) do
- expanded =
- socket.assigns.post_editor_expanded
- |> Map.get(post_id, %{metadata: false, excerpt: false})
- |> Map.put_new(:metadata, false)
- |> Map.put_new(:excerpt, false)
- |> updater.()
-
- assign(socket, :post_editor_expanded, Map.put(socket.assigns.post_editor_expanded, post_id, expanded))
- end
-
- defp put_nested_map(map, key, nested_key, value) do
- Map.update(map, key, %{nested_key => value}, &Map.put(&1, nested_key, value))
- end
-
- defp delete_nested_map(map, key, nested_key) do
- case Map.get(map, key) do
- nil -> map
- nested ->
- case Map.delete(nested, nested_key) do
- emptied when map_size(emptied) == 0 -> Map.delete(map, key)
- remaining -> Map.put(map, key, remaining)
- end
- end
+ PostEditor.assign_socket(socket)
end
@@ -1519,177 +950,6 @@ defmodule BDS.Desktop.ShellLive do
|> reload_shell(workbench)
end
- defp merge_sidebar_ui_state(socket, view_id, sidebar_data) do
- filters = Map.get(sidebar_data, :filters)
-
- if is_map(filters) and Map.get(filters, :enabled) do
- panel_state = sidebar_filter_panel_state(socket, view_id)
-
- Map.put(sidebar_data, :filters, Map.merge(filters, %{
- filter_panel_visible: panel_state.visible,
- archive_collapsed: panel_state.archive_collapsed,
- tags_collapsed: panel_state.tags_collapsed,
- categories_collapsed: panel_state.categories_collapsed,
- expanded_year: panel_state.expanded_year
- }))
- else
- sidebar_data
- end
- end
-
- defp sidebar_filter_panel_state(socket, view_id) do
- default_state = default_sidebar_filter_panel_state()
-
- case Map.get(socket.assigns.sidebar_filter_panels, view_id) do
- state when is_map(state) -> Map.merge(default_state, state)
- visible when is_boolean(visible) -> Map.put(default_state, :visible, visible)
- _other -> default_state
- end
- end
-
- defp put_sidebar_filter_panel_state(socket, updater) do
- view_id = Atom.to_string(socket.assigns.workbench.active_view)
- state = socket |> sidebar_filter_panel_state(view_id) |> updater.()
- assign(socket, :sidebar_filter_panels, Map.put(socket.assigns.sidebar_filter_panels, view_id, state))
- end
-
- defp default_sidebar_filter_panel_state do
- %{
- visible: false,
- archive_collapsed: true,
- tags_collapsed: true,
- categories_collapsed: true,
- expanded_year: nil
- }
- end
-
- defp current_sidebar_filters(socket, view_id) do
- socket.assigns.sidebar_filters_by_view
- |> Map.get(view_id, %{})
- |> normalize_sidebar_filters(socket.assigns[:sidebar_data])
- end
-
- defp normalize_sidebar_filters(filters, sidebar_data) do
- max_items = sidebar_page_size(sidebar_data)
-
- %{
- search: normalize_filter_string(Map.get(filters, :search)),
- year: Map.get(filters, :year),
- month: Map.get(filters, :month),
- tags: Map.get(filters, :tags, []),
- categories: Map.get(filters, :categories, []),
- display_limit: max(Map.get(filters, :display_limit, max_items) || max_items, max_items)
- }
- end
-
- defp put_sidebar_filters(socket, updater) do
- view_id = Atom.to_string(socket.assigns.workbench.active_view)
- filters = current_sidebar_filters(socket, view_id) |> updater.() |> normalize_sidebar_filters(socket.assigns.sidebar_data)
- assign(socket, :sidebar_filters_by_view, Map.put(socket.assigns.sidebar_filters_by_view, view_id, filters))
- end
-
- defp toggle_filter_value(filters, key, value) do
- values = Map.get(filters, key, [])
-
- next_values =
- if value in values do
- List.delete(values, value)
- else
- values ++ [value]
- end
-
- Map.put(filters, key, next_values)
- end
-
- defp group_year_month_counts(entries) do
- entries
- |> Enum.group_by(& &1.year)
- |> Enum.map(fn {year, months} ->
- %{
- year: year,
- count: Enum.reduce(months, 0, fn entry, acc -> acc + (entry.count || 0) end),
- months: Enum.sort_by(months, &-&1.month)
- }
- end)
- |> Enum.sort_by(&-&1.year)
- end
-
- defp sidebar_filter_tag_color(filters_config, tag) do
- filters_config
- |> Map.get(:available_tag_colors, %{})
- |> Map.get(tag)
- |> normalize_sidebar_filter_color()
- end
-
- defp sidebar_filter_chip_style(filters_config, tag) do
- case sidebar_filter_tag_color(filters_config, tag) do
- nil -> nil
- color -> "background-color: #{color}; color: #{sidebar_filter_contrast_color(color)}; border-color: #{color};"
- end
- end
-
- defp normalize_sidebar_filter_color(nil), do: nil
- defp normalize_sidebar_filter_color(""), do: nil
-
- defp normalize_sidebar_filter_color("#" <> rest = color) when byte_size(rest) == 6 do
- if String.match?(rest, ~r/\A[0-9a-fA-F]{6}\z/), do: color, else: nil
- end
-
- defp normalize_sidebar_filter_color(_color), do: nil
-
- defp sidebar_filter_contrast_color("#" <> rgb) do
- <> = rgb
- {red, _} = Integer.parse(r, 16)
- {green, _} = Integer.parse(g, 16)
- {blue, _} = Integer.parse(b, 16)
- luminance = (red * 299 + green * 587 + blue * 114) / 1000
- if luminance > 150, do: "#1e1e1e", else: "#ffffff"
- end
-
- defp sidebar_filter_contrast_color(_color), do: "#ffffff"
-
- defp sidebar_filters_enabled?(sidebar_data) do
- sidebar_data
- |> Map.get(:filters)
- |> then(&(is_map(&1) and Map.get(&1, :enabled, false)))
- end
-
- defp sidebar_filters_visible?(sidebar_data) do
- sidebar_data
- |> Map.get(:filters, %{})
- |> Map.get(:filter_panel_visible, false)
- end
-
- defp normalize_filter_string(nil), do: nil
-
- defp normalize_filter_string(value) do
- value
- |> to_string()
- |> String.trim()
- |> case do
- "" -> nil
- trimmed -> trimmed
- end
- end
-
- defp parse_optional_integer(nil), do: nil
- defp parse_optional_integer(value) when is_integer(value), do: value
-
- defp parse_optional_integer(value) when is_binary(value) do
- case Integer.parse(value) do
- {parsed, _rest} -> parsed
- :error -> nil
- end
- end
-
- defp sidebar_page_size(nil), do: 500
-
- defp sidebar_page_size(sidebar_data) do
- sidebar_data
- |> Map.get(:filters, %{})
- |> Map.get(:max_items, 500)
- end
-
defp set_page_language(socket, language) do
codes = Enum.map(socket.assigns[:supported_ui_languages] || ShellData.supported_ui_languages(), & &1.code)
@@ -2043,11 +1303,6 @@ defmodule BDS.Desktop.ShellLive do
end
end
- defp sidebar_item_selected?(workbench, route, id) do
- route_atom = sidebar_route_atom(route)
- workbench.active_tab == {route_atom, tab_id_for_route(route_atom, id)}
- end
-
defp tab_title(nil, _tab_meta), do: translated("Dashboard")
defp tab_title(tab, tab_meta) do
@@ -2152,13 +1407,4 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:shell_overlay, nil)
end
- defp media_thumbnail_glyph(mime_type) do
- case String.split(to_string(mime_type || ""), "/", parts: 2) do
- ["image", _rest] -> "IMG"
- ["video", _rest] -> "VID"
- ["audio", _rest] -> "AUD"
- ["application", _rest] -> "DOC"
- _other -> "FILE"
- end
- end
end
diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex
index 2232ad9..ac6a8cb 100644
--- a/lib/bds/desktop/shell_live/index.html.heex
+++ b/lib/bds/desktop/shell_live/index.html.heex
@@ -166,12 +166,12 @@
<%= String.upcase(sidebar_header_label(@sidebar_header)) %>
- <%= if sidebar_filters_enabled?(@sidebar_data) do %>
+ <%= if ShellSidebarComponents.filters_enabled?(@sidebar_data) do %>