fix: A1-13 wire git sidebar to BDS.Git with branch, changes, history, and actions
This commit is contained in:
@@ -84,6 +84,8 @@ defmodule BDS.Desktop.ShellLive do
|
||||
"load_more_sidebar"
|
||||
]
|
||||
|
||||
@git_action_events ["git_fetch", "git_pull", "git_push", "git_prune_lfs"]
|
||||
|
||||
@layout_menu_actions MapSet.new([
|
||||
:toggle_sidebar,
|
||||
:toggle_panel,
|
||||
@@ -192,7 +194,8 @@ defmodule BDS.Desktop.ShellLive do
|
||||
end
|
||||
|
||||
def handle_event("toggle_assistant_sidebar", _params, socket) do
|
||||
{:noreply, refresh_layout(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))}
|
||||
{:noreply,
|
||||
refresh_layout(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))}
|
||||
end
|
||||
|
||||
def handle_event("select_view", %{"view" => view_id}, socket) do
|
||||
@@ -237,6 +240,20 @@ defmodule BDS.Desktop.ShellLive do
|
||||
SidebarEvents.handle(socket, event, params, &refresh_sidebar/2)
|
||||
end
|
||||
|
||||
def handle_event(event, _params, socket) when event in @git_action_events do
|
||||
{:noreply, run_git_action(socket, event)}
|
||||
end
|
||||
|
||||
def handle_event("git_commit", params, socket) do
|
||||
message = params |> get_in(["git", "message"]) |> to_string() |> String.trim()
|
||||
{:noreply, commit_git(socket, message)}
|
||||
end
|
||||
|
||||
def handle_event("git_initialize", params, socket) do
|
||||
remote_url = params |> get_in(["git", "remote_url"]) |> normalize_git_remote_url()
|
||||
{:noreply, initialize_git(socket, remote_url)}
|
||||
end
|
||||
|
||||
def handle_event("create_sidebar_item", %{"kind" => kind}, socket) do
|
||||
{:noreply, create_sidebar_item(socket, kind)}
|
||||
end
|
||||
@@ -424,7 +441,9 @@ defmodule BDS.Desktop.ShellLive do
|
||||
|
||||
Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn ->
|
||||
case FilePicker.choose_files(dgettext("ui", "Add Gallery Images"),
|
||||
image_only: true, multiple: true) do
|
||||
image_only: true,
|
||||
multiple: true
|
||||
) do
|
||||
{:ok, paths} when is_list(paths) and paths != [] ->
|
||||
GalleryImport.start(paths, project_id, post_id, language, concurrency_limit, parent)
|
||||
|
||||
@@ -623,7 +642,13 @@ defmodule BDS.Desktop.ShellLive do
|
||||
|
||||
def handle_info({:add_image_processed, title}, socket) do
|
||||
{:noreply,
|
||||
append_output_entry(socket, dgettext("ui", "Add Gallery Images"), dgettext("ui", "Added %{title}", title: title), nil, "info")}
|
||||
append_output_entry(
|
||||
socket,
|
||||
dgettext("ui", "Add Gallery Images"),
|
||||
dgettext("ui", "Added %{title}", title: title),
|
||||
nil,
|
||||
"info"
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info({:add_images_complete, count}, socket) do
|
||||
@@ -660,7 +685,13 @@ defmodule BDS.Desktop.ShellLive do
|
||||
|
||||
def handle_info({:add_images_error, reason}, socket) do
|
||||
{:noreply,
|
||||
append_output_entry(socket, dgettext("ui", "Add Gallery Images"), inspect(reason), nil, "error")}
|
||||
append_output_entry(
|
||||
socket,
|
||||
dgettext("ui", "Add Gallery Images"),
|
||||
inspect(reason),
|
||||
nil,
|
||||
"error"
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_info({:add_image_error, path, reason}, socket) do
|
||||
@@ -668,7 +699,10 @@ defmodule BDS.Desktop.ShellLive do
|
||||
append_output_entry(
|
||||
socket,
|
||||
dgettext("ui", "Add Gallery Images"),
|
||||
dgettext("ui", "Failed to process %{path}: %{reason}", path: Path.basename(path), reason: inspect(reason)),
|
||||
dgettext("ui", "Failed to process %{path}: %{reason}",
|
||||
path: Path.basename(path),
|
||||
reason: inspect(reason)
|
||||
),
|
||||
nil,
|
||||
"error"
|
||||
)}
|
||||
@@ -696,13 +730,17 @@ defmodule BDS.Desktop.ShellLive do
|
||||
defp refresh_layout(socket, workbench) do
|
||||
git_badge_count = socket.assigns[:git_badge_count] || 0
|
||||
activity_buttons = Workbench.activity_buttons(workbench, git_badge_count)
|
||||
task_status = socket.assigns[:task_status] || %{running_task_message: nil, running_task_overflow: nil}
|
||||
|
||||
task_status =
|
||||
socket.assigns[:task_status] || %{running_task_message: nil, running_task_overflow: nil}
|
||||
|
||||
dashboard = socket.assigns[:dashboard] || BDS.UI.Dashboard.empty_snapshot()
|
||||
page_language = socket.assigns[:page_language] || ShellData.ui_language()
|
||||
offline_mode = Map.get(socket.assigns, :offline_mode, true)
|
||||
sidebar_data = socket.assigns[:sidebar_data] || %{}
|
||||
current_tab = current_tab(workbench)
|
||||
prev_tab = socket.assigns[:current_tab]
|
||||
|
||||
prev_panel_tab =
|
||||
case socket.assigns[:workbench] do
|
||||
%Workbench{panel: %{active_tab: tab}} -> tab
|
||||
@@ -1017,6 +1055,122 @@ defmodule BDS.Desktop.ShellLive do
|
||||
|> push_url_state()
|
||||
end
|
||||
|
||||
defp run_git_action(socket, event) do
|
||||
project_id = current_project_id(socket)
|
||||
|
||||
{label, result} =
|
||||
case event do
|
||||
"git_fetch" -> {dgettext("ui", "Fetch"), git_call(project_id, &BDS.Git.fetch/1)}
|
||||
"git_pull" -> {dgettext("ui", "Pull"), git_call(project_id, &BDS.Git.pull/1)}
|
||||
"git_push" -> {dgettext("ui", "Push"), git_call(project_id, &BDS.Git.push/1)}
|
||||
"git_prune_lfs" -> {dgettext("ui", "Prune LFS"), prune_lfs(project_id)}
|
||||
end
|
||||
|
||||
socket
|
||||
|> append_git_result(label, result)
|
||||
|> refresh_sidebar(socket.assigns.workbench)
|
||||
end
|
||||
|
||||
defp commit_git(socket, "") do
|
||||
socket
|
||||
|> append_output_entry(
|
||||
dgettext("ui", "Commit"),
|
||||
dgettext("ui", "Commit message is required"),
|
||||
nil,
|
||||
"error"
|
||||
)
|
||||
|> refresh_sidebar(socket.assigns.workbench)
|
||||
end
|
||||
|
||||
defp commit_git(socket, message) do
|
||||
case git_call(current_project_id(socket), &BDS.Git.commit_all(&1, message)) do
|
||||
{:ok, _result} ->
|
||||
workbench = close_git_diff_tabs(socket.assigns.workbench)
|
||||
tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{})
|
||||
|
||||
socket
|
||||
|> assign(:tab_meta, tab_meta)
|
||||
|> append_output_entry(dgettext("ui", "Commit"), message)
|
||||
|> refresh_sidebar(workbench)
|
||||
|> push_url_state()
|
||||
|
||||
{:error, reason} ->
|
||||
socket
|
||||
|> append_output_entry(dgettext("ui", "Commit"), format_git_error(reason), nil, "error")
|
||||
|> refresh_sidebar(socket.assigns.workbench)
|
||||
end
|
||||
end
|
||||
|
||||
defp initialize_git(socket, remote_url) do
|
||||
project_id = current_project_id(socket)
|
||||
|
||||
case git_call(project_id, &BDS.Git.initialize_repo/1) do
|
||||
{:ok, _repo} ->
|
||||
_ = maybe_set_git_remote(project_id, remote_url)
|
||||
|
||||
socket
|
||||
|> append_output_entry(
|
||||
dgettext("ui", "Initialize Git"),
|
||||
dgettext("ui", "Repository initialized")
|
||||
)
|
||||
|> refresh_sidebar(socket.assigns.workbench)
|
||||
|
||||
{:error, reason} ->
|
||||
socket
|
||||
|> append_output_entry(
|
||||
dgettext("ui", "Initialize Git"),
|
||||
format_git_error(reason),
|
||||
nil,
|
||||
"error"
|
||||
)
|
||||
|> refresh_sidebar(socket.assigns.workbench)
|
||||
end
|
||||
end
|
||||
|
||||
defp git_call(nil, _fun), do: {:error, :no_project}
|
||||
defp git_call("default", _fun), do: {:error, :no_project}
|
||||
defp git_call(project_id, fun) when is_binary(project_id), do: fun.(project_id)
|
||||
|
||||
defp prune_lfs(nil), do: {:error, :no_project}
|
||||
defp prune_lfs("default"), do: {:error, :no_project}
|
||||
|
||||
defp prune_lfs(project_id) when is_binary(project_id),
|
||||
do: BDS.Git.prune_lfs_cache(project_id, 10)
|
||||
|
||||
defp maybe_set_git_remote(_project_id, nil), do: :ok
|
||||
|
||||
defp maybe_set_git_remote(project_id, remote_url),
|
||||
do: BDS.Git.set_remote(project_id, remote_url)
|
||||
|
||||
defp append_git_result(socket, label, {:ok, _result}) do
|
||||
append_output_entry(socket, label, dgettext("ui", "Done"))
|
||||
end
|
||||
|
||||
defp append_git_result(socket, label, {:error, reason}) do
|
||||
append_output_entry(socket, label, format_git_error(reason), nil, "error")
|
||||
end
|
||||
|
||||
defp format_git_error(:no_project), do: dgettext("ui", "No active project")
|
||||
defp format_git_error(%{message: message}) when is_binary(message), do: message
|
||||
defp format_git_error(%{guidance: guidance}) when is_binary(guidance), do: guidance
|
||||
defp format_git_error({:git_failed, message}) when is_binary(message), do: message
|
||||
defp format_git_error(reason), do: inspect(reason)
|
||||
|
||||
defp close_git_diff_tabs(workbench) do
|
||||
workbench.tabs
|
||||
|> Enum.filter(&(&1.type == :git_diff))
|
||||
|> Enum.reduce(workbench, fn tab, wb -> Workbench.close_tab(wb, :git_diff, tab.id) end)
|
||||
end
|
||||
|
||||
defp current_project_id(socket), do: (socket.assigns[:projects] || %{})[:active_project_id]
|
||||
|
||||
defp normalize_git_remote_url(value) do
|
||||
case value |> to_string() |> String.trim() do
|
||||
"" -> nil
|
||||
url -> url
|
||||
end
|
||||
end
|
||||
|
||||
defp sidebar_create_action(view), do: SidebarCreate.action(view)
|
||||
|
||||
defp set_page_language(socket, language) do
|
||||
|
||||
@@ -257,6 +257,7 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
|
||||
"media_grid" -> render_media_sidebar(assigns)
|
||||
"entity_list" -> render_entity_sidebar(assigns)
|
||||
"nav_list" -> render_nav_sidebar(assigns)
|
||||
"git" -> render_git_sidebar(assigns)
|
||||
_other -> render_default_sidebar(assigns)
|
||||
end
|
||||
end
|
||||
@@ -483,6 +484,141 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_git_sidebar(assigns) do
|
||||
assigns = assign(assigns, :git_state, Map.get(assigns.sidebar_data, :git_state, "not_a_repo"))
|
||||
|
||||
~H"""
|
||||
<div class="git-sidebar">
|
||||
<%= if @git_state == "active" do %>
|
||||
<%= render_git_active(assigns) %>
|
||||
<% else %>
|
||||
<%= render_git_not_a_repo(assigns) %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_git_not_a_repo(assigns) do
|
||||
~H"""
|
||||
<section class="git-section git-not-a-repo">
|
||||
<p class="git-empty-hint"><%= dgettext("ui", "This project is not a Git repository yet.") %></p>
|
||||
<form class="git-init-form flex flex-col gap-2" data-testid="git-init-form" phx-submit="git_initialize">
|
||||
<input
|
||||
type="text"
|
||||
name="git[remote_url]"
|
||||
placeholder={dgettext("ui", "Remote URL (optional)")}
|
||||
value={Map.get(@sidebar_data, :remote_url) || ""}
|
||||
/>
|
||||
<button class="git-action-button" data-testid="git-initialize" type="submit">
|
||||
<%= dgettext("ui", "Initialize Git") %>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_git_active(assigns) do
|
||||
~H"""
|
||||
<header class="git-header">
|
||||
<div class="git-branch-row flex items-center gap-2">
|
||||
<span class="git-branch-icon">⎇</span>
|
||||
<span class="git-branch" data-testid="git-branch"><%= @sidebar_data.branch %></span>
|
||||
<%= if @sidebar_data.upstream do %>
|
||||
<span class="git-upstream" data-testid="git-upstream"><%= @sidebar_data.upstream %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="git-tracking flex items-center gap-3">
|
||||
<span class="git-ahead" data-testid="git-ahead" title={dgettext("ui", "Ahead")}>↑ <%= @sidebar_data.ahead %></span>
|
||||
<span class="git-behind" data-testid="git-behind" title={dgettext("ui", "Behind")}>↓ <%= @sidebar_data.behind %></span>
|
||||
</div>
|
||||
<div class="git-sync-legend flex items-center gap-3">
|
||||
<span class="git-legend-item"><span class="git-sync-dot git-sync-synced"></span><%= dgettext("ui", "Synced") %></span>
|
||||
<span class="git-legend-item"><span class="git-sync-dot git-sync-local_only"></span><%= dgettext("ui", "Local only") %></span>
|
||||
<span class="git-legend-item"><span class="git-sync-dot git-sync-remote_only"></span><%= dgettext("ui", "Remote only") %></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="git-actions flex items-center gap-2">
|
||||
<button class="git-action-button" data-testid="git-action-fetch" type="button" phx-click="git_fetch" title={dgettext("ui", "Fetch")}><%= dgettext("ui", "Fetch") %></button>
|
||||
<button class="git-action-button" data-testid="git-action-pull" type="button" phx-click="git_pull" title={dgettext("ui", "Pull")}><%= dgettext("ui", "Pull") %></button>
|
||||
<button class="git-action-button" data-testid="git-action-push" type="button" phx-click="git_push" title={dgettext("ui", "Push")}><%= dgettext("ui", "Push") %></button>
|
||||
<button class="git-action-button" data-testid="git-action-prune-lfs" type="button" phx-click="git_prune_lfs" title={dgettext("ui", "Prune LFS")}><%= dgettext("ui", "Prune LFS") %></button>
|
||||
</div>
|
||||
|
||||
<section class="git-section git-changes">
|
||||
<div class="git-section-title">
|
||||
<span><%= dgettext("ui", "Changes") %></span>
|
||||
<span class="git-section-count"><%= length(@sidebar_data.status_files) %></span>
|
||||
</div>
|
||||
|
||||
<form class="git-commit-form flex flex-col gap-2" data-testid="git-commit-form" phx-submit="git_commit">
|
||||
<input type="text" name="git[message]" placeholder={dgettext("ui", "Commit message")} />
|
||||
<button class="git-action-button" data-testid="git-commit" type="submit"><%= dgettext("ui", "Commit") %></button>
|
||||
</form>
|
||||
|
||||
<%= if Enum.any?(@sidebar_data.status_files) do %>
|
||||
<div class="git-status-list flex flex-col">
|
||||
<%= for file <- @sidebar_data.status_files do %>
|
||||
<button
|
||||
class="git-status-file flex items-center justify-between gap-2"
|
||||
data-testid="git-status-file"
|
||||
data-route="git_diff"
|
||||
type="button"
|
||||
title={"#{file.label}: #{file.path}"}
|
||||
phx-click="open_sidebar_item"
|
||||
phx-value-route="git_diff"
|
||||
phx-value-id={"git-diff:" <> file.path}
|
||||
phx-value-title={file.path}
|
||||
phx-value-subtitle={file.label}
|
||||
>
|
||||
<span class="git-status-path"><%= file.path %></span>
|
||||
<span class={"git-status-badge git-status-#{file.status}"}><%= file.code %></span>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="git-empty-hint"><%= dgettext("ui", "No changes") %></p>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section class="git-section git-history">
|
||||
<div class="git-section-title">
|
||||
<span><%= dgettext("ui", "History") %></span>
|
||||
</div>
|
||||
<%= if Enum.any?(@sidebar_data.history_entries) do %>
|
||||
<div class="git-history-list flex flex-col">
|
||||
<%= for entry <- @sidebar_data.history_entries do %>
|
||||
<button
|
||||
class="git-history-entry flex flex-col"
|
||||
data-testid="git-history-entry"
|
||||
data-route="git_diff"
|
||||
type="button"
|
||||
phx-click="open_sidebar_item"
|
||||
phx-value-route="git_diff"
|
||||
phx-value-id={"git-diff:commit:" <> entry.short_hash}
|
||||
phx-value-title={entry.short_hash}
|
||||
phx-value-subtitle={entry.subject || ""}
|
||||
>
|
||||
<span class="git-history-subject"><%= entry.subject %></span>
|
||||
<span class="git-history-meta flex items-center gap-2">
|
||||
<span class={"git-sync-dot git-sync-#{entry.sync_status}"}></span>
|
||||
<span class="git-history-hash"><%= entry.short_hash %></span>
|
||||
<%= if entry.author do %><span class="git-history-author"><%= entry.author %></span><% end %>
|
||||
<%= if entry.date do %><span class="git-history-date"><%= entry.date %></span><% end %>
|
||||
</span>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if @sidebar_data.has_more_history do %>
|
||||
<p class="git-history-more"><%= dgettext("ui", "Older history available") %></p>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="git-empty-hint"><%= dgettext("ui", "No commits yet") %></p>
|
||||
<% end %>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_default_sidebar(assigns) do
|
||||
~H"""
|
||||
<%= for section <- Map.get(@sidebar_data, :sections, []) do %>
|
||||
|
||||
Reference in New Issue
Block a user