From efb0c21e65b1f17edacdecb24a462bbc41e792c0 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 17:51:12 +0300 Subject: [PATCH 01/20] feat: implement main bounty creation form - Added a new button to open a drawer for creating a new bounty. - Introduced a form for bounty creation with fields for type, title, description, and amount. - Updated the changeset to include new fields and validation logic for the bounty form. - Enhanced the navigation module to manage the state of the bounty form visibility. --- .../components/layouts/user.html.heex | 18 +++ lib/algora_web/forms/bounty_form.ex | 118 +++++++++++++++++- lib/algora_web/live/org/nav.ex | 17 ++- 3 files changed, 148 insertions(+), 5 deletions(-) diff --git a/lib/algora_web/components/layouts/user.html.heex b/lib/algora_web/components/layouts/user.html.heex index 76b473e66..0f198a026 100644 --- a/lib/algora_web/components/layouts/user.html.heex +++ b/lib/algora_web/components/layouts/user.html.heex @@ -189,6 +189,24 @@ <% end %> + <%= if main_bounty_form = Map.get(assigns, :main_bounty_form) do %> + <.button phx-click="open_main_bounty_form">Create new bounty + <.drawer + show={@main_bounty_form_open?} + direction="right" + on_cancel="close_main_bounty_form" + > + <.drawer_header> + <.drawer_title>Create new bounty + <.drawer_description> + Create and fund a bounty for an issue + + + <.drawer_content class="mt-4"> + + + + <% end %>
<.link class="group w-fit outline-none" diff --git a/lib/algora_web/forms/bounty_form.ex b/lib/algora_web/forms/bounty_form.ex index 7c44c8f88..73c4607c6 100644 --- a/lib/algora_web/forms/bounty_form.ex +++ b/lib/algora_web/forms/bounty_form.ex @@ -1,6 +1,7 @@ defmodule AlgoraWeb.Forms.BountyForm do @moduledoc false use Ecto.Schema + use AlgoraWeb, :live_view import Ecto.Changeset @@ -12,6 +13,9 @@ defmodule AlgoraWeb.Forms.BountyForm do field :amount, USD field :visibility, Ecto.Enum, values: [:community, :exclusive, :public], default: :public field :shared_with, {:array, :string}, default: [] + field :type, Ecto.Enum, values: [:github, :custom], default: :github + field :title, :string + field :description, :string embeds_one :ticket_ref, TicketRef, primary_key: false do field :owner, :string @@ -21,11 +25,119 @@ defmodule AlgoraWeb.Forms.BountyForm do end end - def changeset(form, attrs \\ %{}) do + def changeset(form, params) do form - |> cast(attrs, [:url, :amount, :visibility, :shared_with]) - |> validate_required([:url, :amount]) + |> cast(params, [:url, :amount, :visibility, :shared_with, :type, :title, :description]) + |> validate_required([:amount, :visibility, :shared_with]) + |> validate_type_fields() |> Validations.validate_money_positive(:amount) |> Validations.validate_ticket_ref(:url, :ticket_ref) end + + defp validate_type_fields(changeset) do + case get_field(changeset, :type) do + "custom" -> validate_required(changeset, [:title, :description]) + _ -> validate_required(changeset, [:url]) + end + end + + def bounty_form(assigns) do + ~H""" + <.form for={@main_bounty_form} phx-submit="create_bounty"> +
+ <.input type="hidden" name="main_bounty_form[visibility]" value="exclusive" /> + <%!-- <.input + type="hidden" + name="main_bounty_form[shared_with][]" + value={ + case @selected_developer do + %User{handle: nil, provider_id: provider_id} -> [to_string(provider_id)] + %User{id: id} -> [id] + end + } + /> --%> + +
+ + +
+ +
+ <.input + label="URL" + field={@main_bounty_form[:url]} + placeholder="https://github.com/owner/repo/issues/123" + /> +
+ +
+ <.input + label="Title" + field={@main_bounty_form[:title]} + placeholder="Brief description of the bounty" + /> + <.input + label="Description" + field={@main_bounty_form[:description]} + type="textarea" + placeholder="Requirements and acceptance criteria" + /> +
+ + <.input label="Amount" icon="tabler-currency-dollar" field={@main_bounty_form[:amount]} /> +
+
+ <.button variant="secondary" phx-click="close_share_drawer" type="button"> + Cancel + + <.button type="submit"> + Share Bounty <.icon name="tabler-arrow-right" class="-mr-1 ml-2 h-4 w-4" /> + +
+ + """ + end end diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index ddd3c56d7..33c196e8c 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -5,6 +5,7 @@ defmodule AlgoraWeb.Org.Nav do import Phoenix.LiveView alias Algora.Organizations + alias AlgoraWeb.Forms.BountyForm alias AlgoraWeb.OrgAuth def on_mount(:default, %{"org_handle" => org_handle} = params, _session, socket) do @@ -34,12 +35,24 @@ defmodule AlgoraWeb.Org.Nav do {:cont, socket |> assign(:screenshot?, not is_nil(params["screenshot"])) - |> assign(:new_bounty_form, to_form(%{"github_issue_url" => "", "amount" => ""})) + |> assign(:main_bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{}))) + |> assign(:main_bounty_form_open?, false) |> assign(:current_org, current_org) |> assign(:current_user_role, current_user_role) |> assign(:nav, nav_items(current_org.handle, current_user_role)) |> assign(:contacts, contacts) - |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)} + |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3) + |> attach_hook(:form_toggle, :handle_event, &handle_form_toggle_event/3)} + end + + def handle_form_toggle_event("open_main_bounty_form", _params, socket) do + dbg("open_main_bounty_form") + {:cont, assign(socket, :main_bounty_form_open?, true)} + end + + def handle_form_toggle_event("close_main_bounty_form", _params, socket) do + dbg("close_main_bounty_form") + {:cont, assign(socket, :main_bounty_form_open?, false)} end defp handle_active_tab_params(_params, _url, socket) do From e16209b66f73694f1728800d27bea1c9f55f51d7 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 18:04:34 +0300 Subject: [PATCH 02/20] feat: add handle_event callbacks to multiple LiveView modules - Implemented a default handle_event function returning {:noreply, socket} in several LiveView modules to manage events without specific handlers. - Updated the bounty form in the dashboard to include a new radio button selection for bounty type, enhancing user interaction. - Refined the layout of the bounty form for improved usability and responsiveness. --- lib/algora_web/live/bounty_live.ex | 5 + lib/algora_web/live/org/bounties_live.ex | 4 + lib/algora_web/live/org/dashboard_live.ex | 105 ++++++++++++++++--- lib/algora_web/live/org/home_live.ex | 5 + lib/algora_web/live/org/leaderboard_live.ex | 5 + lib/algora_web/live/org/nav.ex | 6 +- lib/algora_web/live/org/repo_nav.ex | 15 ++- lib/algora_web/live/org/settings_live.ex | 5 + lib/algora_web/live/org/team_live.ex | 7 ++ lib/algora_web/live/org/transactions_live.ex | 4 + 10 files changed, 139 insertions(+), 22 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index fe647bcfe..2e8a86fa3 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -295,6 +295,11 @@ defmodule AlgoraWeb.BountyLive do end end + @impl true + def handle_event(_event, _params, socket) do + {:noreply, socket} + end + @impl true def render(assigns) do ~H""" diff --git a/lib/algora_web/live/org/bounties_live.ex b/lib/algora_web/live/org/bounties_live.ex index 384e329ff..b8b486769 100644 --- a/lib/algora_web/live/org/bounties_live.ex +++ b/lib/algora_web/live/org/bounties_live.ex @@ -287,6 +287,10 @@ defmodule AlgoraWeb.Org.BountiesLive do end} end + def handle_event(_event, _params, socket) do + {:noreply, socket} + end + def handle_params(params, _uri, socket) do current_org = socket.assigns.current_org current_status = get_current_status(params) diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index 01c45b321..7e5bb7251 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -89,7 +89,8 @@ defmodule AlgoraWeb.Org.DashboardLive do # Will be initialized when chat starts |> assign(:thread, nil) |> assign(:messages, []) - |> assign(:show_chat, false)} + |> assign(:show_chat, false) + |> assign(:bounty_form_type, "github")} else {:ok, redirect(socket, to: ~p"/org/#{current_org.handle}/home")} end @@ -1662,24 +1663,89 @@ defmodule AlgoraWeb.Org.DashboardLive do end } /> - <.input - label="URL" - field={@bounty_form[:url]} - placeholder="https://github.com/owner/repo/issues/123" - /> + +
+ + +
+ +
+ <.input + label="URL" + field={@bounty_form[:url]} + placeholder="https://github.com/owner/repo/issues/123" + /> +
+ +
+ <.input + label="Title" + field={@bounty_form[:title]} + placeholder="Brief description of the bounty" + /> + <.input + label="Description" + field={@bounty_form[:description]} + type="textarea" + placeholder="Requirements and acceptance criteria" + /> +
+ <.input label="Amount" icon="tabler-currency-dollar" field={@bounty_form[:amount]} />
+
+ <.button variant="secondary" phx-click="close_share_drawer" type="button"> + Cancel + + <.button type="submit"> + Share Bounty <.icon name="tabler-arrow-right" class="-mr-1 ml-2 h-4 w-4" /> + +
- -
- <.button variant="secondary" phx-click="close_share_drawer" type="button"> - Cancel - - <.button type="submit"> - Share Bounty <.icon name="tabler-arrow-right" class="-mr-1 ml-2 h-4 w-4" /> - -
""" end @@ -1795,7 +1861,12 @@ defmodule AlgoraWeb.Org.DashboardLive do defp share_drawer(assigns) do ~H""" - <.drawer show={@show_share_drawer} direction="bottom" on_cancel="close_share_drawer"> + <.drawer + show={@show_share_drawer} + direction="bottom" + on_cancel="close_share_drawer" + class={if @share_drawer_type == "bounty", do: "h-[100svh]"} + > <.share_drawer_header :if={@selected_developer} selected_developer={@selected_developer} @@ -1815,6 +1886,7 @@ defmodule AlgoraWeb.Org.DashboardLive do bounty_form={@bounty_form} tip_form={@tip_form} contract_form={@contract_form} + bounty_form_type={@bounty_form_type} /> <.alert @@ -1838,6 +1910,7 @@ defmodule AlgoraWeb.Org.DashboardLive do bounty_form={@bounty_form} tip_form={@tip_form} contract_form={@contract_form} + bounty_form_type={@bounty_form_type} /> <% end %> diff --git a/lib/algora_web/live/org/home_live.ex b/lib/algora_web/live/org/home_live.ex index d8dea6c98..982502904 100644 --- a/lib/algora_web/live/org/home_live.ex +++ b/lib/algora_web/live/org/home_live.ex @@ -231,6 +231,11 @@ defmodule AlgoraWeb.Org.HomeLive do """ end + @impl true + def handle_event(_event, _params, socket) do + {:noreply, socket} + end + defp social_links do [ {:website, "tabler-world"}, diff --git a/lib/algora_web/live/org/leaderboard_live.ex b/lib/algora_web/live/org/leaderboard_live.ex index cdadbae33..e39b53e6b 100644 --- a/lib/algora_web/live/org/leaderboard_live.ex +++ b/lib/algora_web/live/org/leaderboard_live.ex @@ -83,4 +83,9 @@ defmodule AlgoraWeb.Org.LeaderboardLive do """ end + + @impl true + def handle_event(_event, _params, socket) do + {:noreply, socket} + end end diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index 33c196e8c..650a31c01 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -45,13 +45,11 @@ defmodule AlgoraWeb.Org.Nav do |> attach_hook(:form_toggle, :handle_event, &handle_form_toggle_event/3)} end - def handle_form_toggle_event("open_main_bounty_form", _params, socket) do - dbg("open_main_bounty_form") + defp handle_form_toggle_event("open_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, true)} end - def handle_form_toggle_event("close_main_bounty_form", _params, socket) do - dbg("close_main_bounty_form") + defp handle_form_toggle_event("close_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, false)} end diff --git a/lib/algora_web/live/org/repo_nav.ex b/lib/algora_web/live/org/repo_nav.ex index f42fcf2e1..07cbef1c1 100644 --- a/lib/algora_web/live/org/repo_nav.ex +++ b/lib/algora_web/live/org/repo_nav.ex @@ -5,6 +5,7 @@ defmodule AlgoraWeb.Org.RepoNav do import Phoenix.LiveView alias Algora.Organizations + alias AlgoraWeb.Forms.BountyForm alias AlgoraWeb.OrgAuth def on_mount(:default, %{"repo_owner" => repo_owner} = params, _session, socket) do @@ -15,12 +16,22 @@ defmodule AlgoraWeb.Org.RepoNav do {:cont, socket |> assign(:screenshot?, not is_nil(params["screenshot"])) - |> assign(:new_bounty_form, to_form(%{"github_issue_url" => "", "amount" => ""})) + |> assign(:main_bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{}))) + |> assign(:main_bounty_form_open?, false) |> assign(:current_org, current_org) |> assign(:current_user_role, current_user_role) |> assign(:nav, nav_items(current_org.handle, current_user_role)) |> assign(:contacts, []) - |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3)} + |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3) + |> attach_hook(:form_toggle, :handle_event, &handle_form_toggle_event/3)} + end + + defp handle_form_toggle_event("open_main_bounty_form", _params, socket) do + {:cont, assign(socket, :main_bounty_form_open?, true)} + end + + defp handle_form_toggle_event("close_main_bounty_form", _params, socket) do + {:cont, assign(socket, :main_bounty_form_open?, false)} end defp handle_active_tab_params(_params, _url, socket) do diff --git a/lib/algora_web/live/org/settings_live.ex b/lib/algora_web/live/org/settings_live.ex index d94496902..c2a76306d 100644 --- a/lib/algora_web/live/org/settings_live.ex +++ b/lib/algora_web/live/org/settings_live.ex @@ -325,6 +325,11 @@ defmodule AlgoraWeb.Org.SettingsLive do end end + @impl true + def handle_event(_event, _params, socket) do + {:noreply, socket} + end + @impl true def handle_params(params, _url, socket) do {:noreply, apply_action(socket, socket.assigns.live_action, params)} diff --git a/lib/algora_web/live/org/team_live.ex b/lib/algora_web/live/org/team_live.ex index 99664d14d..e4a10d4f8 100644 --- a/lib/algora_web/live/org/team_live.ex +++ b/lib/algora_web/live/org/team_live.ex @@ -5,6 +5,7 @@ defmodule AlgoraWeb.Org.TeamLive do alias Algora.Accounts.User alias Algora.Organizations + @impl true def mount(%{"org_handle" => handle}, _session, socket) do org = Organizations.get_org_by_handle!(handle) members = Organizations.list_org_members(org) @@ -18,6 +19,7 @@ defmodule AlgoraWeb.Org.TeamLive do |> assign(:contractors, contractors)} end + @impl true def render(assigns) do ~H"""
@@ -115,4 +117,9 @@ defmodule AlgoraWeb.Org.TeamLive do
""" end + + @impl true + def handle_event(_event, _params, socket) do + {:noreply, socket} + end end diff --git a/lib/algora_web/live/org/transactions_live.ex b/lib/algora_web/live/org/transactions_live.ex index d24b4a188..276dc54c8 100644 --- a/lib/algora_web/live/org/transactions_live.ex +++ b/lib/algora_web/live/org/transactions_live.ex @@ -131,6 +131,10 @@ defmodule AlgoraWeb.Org.TransactionsLive do end end + def handle_event(_event, _params, socket) do + {:noreply, socket} + end + defp assign_transactions(socket) do transactions = Payments.list_transactions( From 552b0f64bba838beae083dda32855ac3f16152f0 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 18:41:35 +0300 Subject: [PATCH 03/20] feat: enhance bounty form with dynamic type selection - Added a new function to provide type options for the bounty form, allowing users to select between "GitHub issue" and "Custom". - Refactored the form layout to dynamically render radio buttons for type selection, improving user interaction. - Updated the changeset validation to ensure the selected type is part of the defined options. - Introduced data attributes for better handling of form visibility based on the selected type. --- lib/algora_web/forms/bounty_form.ex | 88 ++++++++++++++--------------- lib/algora_web/live/org/nav.ex | 3 + lib/algora_web/live/org/repo_nav.ex | 3 + 3 files changed, 49 insertions(+), 45 deletions(-) diff --git a/lib/algora_web/forms/bounty_form.ex b/lib/algora_web/forms/bounty_form.ex index 73c4607c6..eaee2d389 100644 --- a/lib/algora_web/forms/bounty_form.ex +++ b/lib/algora_web/forms/bounty_form.ex @@ -25,6 +25,10 @@ defmodule AlgoraWeb.Forms.BountyForm do end end + def type_options do + [{"GitHub issue", "github"}, {"Custom", "custom"}] + end + def changeset(form, params) do form |> cast(params, [:url, :amount, :visibility, :shared_with, :type, :title, :description]) @@ -32,6 +36,7 @@ defmodule AlgoraWeb.Forms.BountyForm do |> validate_type_fields() |> Validations.validate_money_positive(:amount) |> Validations.validate_ticket_ref(:url, :ticket_ref) + |> validate_subset(:type, Enum.map(type_options(), &elem(&1, 1))) end defp validate_type_fields(changeset) do @@ -43,7 +48,7 @@ defmodule AlgoraWeb.Forms.BountyForm do def bounty_form(assigns) do ~H""" - <.form for={@main_bounty_form} phx-submit="create_bounty"> + <.form id="main-bounty-form" for={@main_bounty_form} phx-submit="create_bounty">
<.input type="hidden" name="main_bounty_form[visibility]" value="exclusive" /> <%!-- <.input @@ -58,51 +63,39 @@ defmodule AlgoraWeb.Forms.BountyForm do /> --%>
- - + + {label} + <.icon + name="tabler-check" + class="invisible size-5 text-primary group-has-[:checked]:visible" + /> + + + <% end %>
-
+
<.input label="URL" field={@main_bounty_form[:url]} @@ -111,8 +104,13 @@ defmodule AlgoraWeb.Forms.BountyForm do
<.input label="Title" diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index 650a31c01..f5bdba70a 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -45,6 +45,9 @@ defmodule AlgoraWeb.Org.Nav do |> attach_hook(:form_toggle, :handle_event, &handle_form_toggle_event/3)} end + # TODO: handle submit + # TODO: handle validate + defp handle_form_toggle_event("open_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, true)} end diff --git a/lib/algora_web/live/org/repo_nav.ex b/lib/algora_web/live/org/repo_nav.ex index 07cbef1c1..ca7dddcd5 100644 --- a/lib/algora_web/live/org/repo_nav.ex +++ b/lib/algora_web/live/org/repo_nav.ex @@ -26,6 +26,9 @@ defmodule AlgoraWeb.Org.RepoNav do |> attach_hook(:form_toggle, :handle_event, &handle_form_toggle_event/3)} end + # TODO: handle submit + # TODO: handle validate + defp handle_form_toggle_event("open_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, true)} end From 8dee1196d529ff754e55c4cf1baaf8ad2dfb1983 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 18:57:42 +0300 Subject: [PATCH 04/20] refactor: update bounty form and layout components - Changed the `bounty_form` function to use a more generic `form` assign, improving consistency across the form's implementation. - Updated the form's HTML structure to reflect the new assign, enhancing clarity and maintainability. - Adjusted the import of `Tails` for class helpers to improve organization within the `AlgoraWeb` module. - Added a new event handler for `create_bounty_main` to manage form submission events effectively. --- lib/algora_web.ex | 10 ++++--- .../components/layouts/user.html.heex | 2 +- lib/algora_web/forms/bounty_form.ex | 28 ++++++++----------- lib/algora_web/live/org/nav.ex | 5 ++++ 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/algora_web.ex b/lib/algora_web.ex index e857865b3..be9c2c519 100644 --- a/lib/algora_web.ex +++ b/lib/algora_web.ex @@ -56,8 +56,6 @@ defmodule AlgoraWeb do use Phoenix.LiveView, layout: {AlgoraWeb.Layouts, :app} - import Tails, only: [classes: 1] - unquote(html_helpers()) end end @@ -87,13 +85,17 @@ defmodule AlgoraWeb do quote do use Gettext, backend: AlgoraWeb.Gettext + # Core UI components and translation import AlgoraWeb.CoreComponents + + # Enable rendering Svelte components import LiveSvelte + # HTML escaping functionality import Phoenix.HTML - # Core UI components and translation - # Enable rendering Svelte components + # class helpers + import Tails, only: [classes: 1] # Shortcut for generating JS commands alias Phoenix.LiveView.JS diff --git a/lib/algora_web/components/layouts/user.html.heex b/lib/algora_web/components/layouts/user.html.heex index 0f198a026..3431c4f45 100644 --- a/lib/algora_web/components/layouts/user.html.heex +++ b/lib/algora_web/components/layouts/user.html.heex @@ -203,7 +203,7 @@ <.drawer_content class="mt-4"> - + <% end %> diff --git a/lib/algora_web/forms/bounty_form.ex b/lib/algora_web/forms/bounty_form.ex index eaee2d389..97b27c83c 100644 --- a/lib/algora_web/forms/bounty_form.ex +++ b/lib/algora_web/forms/bounty_form.ex @@ -1,7 +1,7 @@ defmodule AlgoraWeb.Forms.BountyForm do @moduledoc false use Ecto.Schema - use AlgoraWeb, :live_view + use AlgoraWeb, :html import Ecto.Changeset @@ -48,12 +48,12 @@ defmodule AlgoraWeb.Forms.BountyForm do def bounty_form(assigns) do ~H""" - <.form id="main-bounty-form" for={@main_bounty_form} phx-submit="create_bounty"> + <.form id="main-bounty-form" for={@form} phx-submit="create_bounty_main">
- <.input type="hidden" name="main_bounty_form[visibility]" value="exclusive" /> + <%!-- <.input type="hidden" name="bounty_form[visibility]" value="exclusive" /> --%> <%!-- <.input type="hidden" - name="main_bounty_form[shared_with][]" + name="bounty_form[shared_with][]" value={ case @selected_developer do %User{handle: nil, provider_id: provider_id} -> [to_string(provider_id)] @@ -71,8 +71,8 @@ defmodule AlgoraWeb.Forms.BountyForm do ]}> <.input type="radio" - field={@main_bounty_form[:type]} - checked={to_string(get_field(@main_bounty_form.source, :type)) == value} + field={@form[:type]} + checked={to_string(get_field(@form.source, :type)) == value} value={value} class="sr-only" phx-click={ @@ -94,11 +94,11 @@ defmodule AlgoraWeb.Forms.BountyForm do
<.input label="URL" - field={@main_bounty_form[:url]} + field={@form[:url]} placeholder="https://github.com/owner/repo/issues/123" />
@@ -107,25 +107,21 @@ defmodule AlgoraWeb.Forms.BountyForm do data-tab="custom" class={ classes([ - to_string(get_field(@main_bounty_form.source, :type)) != "custom" && "hidden", + to_string(get_field(@form.source, :type)) != "custom" && "hidden", "space-y-4" ]) } > - <.input - label="Title" - field={@main_bounty_form[:title]} - placeholder="Brief description of the bounty" - /> + <.input label="Title" field={@form[:title]} placeholder="Brief description of the bounty" /> <.input label="Description" - field={@main_bounty_form[:description]} + field={@form[:description]} type="textarea" placeholder="Requirements and acceptance criteria" />
- <.input label="Amount" icon="tabler-currency-dollar" field={@main_bounty_form[:amount]} /> + <.input label="Amount" icon="tabler-currency-dollar" field={@form[:amount]} />
<.button variant="secondary" phx-click="close_share_drawer" type="button"> diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index f5bdba70a..1315c538c 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -48,6 +48,11 @@ defmodule AlgoraWeb.Org.Nav do # TODO: handle submit # TODO: handle validate + defp handle_form_toggle_event("create_bounty_main", params, socket) do + dbg(params) + {:cont, socket} + end + defp handle_form_toggle_event("open_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, true)} end From da7255d5991ef7246d0bf35ad7ea8a978c8453b6 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 19:09:46 +0300 Subject: [PATCH 05/20] fix: update warning messages for bounty creation errors - Changed the warning message for existing bounties to improve clarity and user experience across multiple LiveView modules. - Ensured consistency in messaging by standardizing the text in `home_live.ex`, `swift_bounties_live.ex`, and `dashboard_live.ex` files. --- lib/algora_web/live/home_live.ex | 2 +- lib/algora_web/live/org/dashboard_live.ex | 2 +- lib/algora_web/live/org/nav.ex | 46 ++++++++++++++++++++-- lib/algora_web/live/swift_bounties_live.ex | 2 +- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/lib/algora_web/live/home_live.ex b/lib/algora_web/live/home_live.ex index 9bc9d6081..191d4cb52 100644 --- a/lib/algora_web/live/home_live.ex +++ b/lib/algora_web/live/home_live.ex @@ -1058,7 +1058,7 @@ defmodule AlgoraWeb.HomeLive do |> redirect(to: AlgoraWeb.UserAuth.generate_login_path(user.email))} {:error, :already_exists} -> - {:noreply, put_flash(socket, :warning, "You have already created a bounty for this ticket")} + {:noreply, put_flash(socket, :warning, "You already have a bounty for this ticket")} {:error, _reason} -> {:noreply, put_flash(socket, :error, "Something went wrong")} diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index 7e5bb7251..c933b1f2d 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -634,7 +634,7 @@ defmodule AlgoraWeb.Org.DashboardLive do {:noreply, assign(socket, :bounty_form, to_form(changeset))} {:error, :already_exists} -> - {:noreply, put_flash(socket, :warning, "You have already created a bounty for this ticket")} + {:noreply, put_flash(socket, :warning, "You already have a bounty for this ticket")} {:error, _reason} -> {:noreply, put_flash(socket, :error, "Something went wrong")} diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index 1315c538c..9645845cd 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -2,8 +2,10 @@ defmodule AlgoraWeb.Org.Nav do @moduledoc false use Phoenix.Component + import Ecto.Changeset import Phoenix.LiveView + alias Algora.Bounties alias Algora.Organizations alias AlgoraWeb.Forms.BountyForm alias AlgoraWeb.OrgAuth @@ -48,9 +50,47 @@ defmodule AlgoraWeb.Org.Nav do # TODO: handle submit # TODO: handle validate - defp handle_form_toggle_event("create_bounty_main", params, socket) do - dbg(params) - {:cont, socket} + defp handle_form_toggle_event("create_bounty_main" = event, %{"bounty_form" => params} = unsigned_params, socket) do + if socket.assigns.has_fresh_token? do + changeset = %BountyForm{} |> BountyForm.changeset(params) |> Map.put(:action, :validate) + + amount = get_field(changeset, :amount) + ticket_ref = get_field(changeset, :ticket_ref) + + with %{valid?: true} <- changeset, + {:ok, _bounty} <- + Bounties.create_bounty( + %{ + creator: socket.assigns.current_user, + owner: socket.assigns.current_org, + amount: amount, + ticket_ref: %{ + owner: ticket_ref.owner, + repo: ticket_ref.repo, + number: ticket_ref.number + } + }, + strategy: :create, + visibility: get_field(changeset, :visibility), + shared_with: get_field(changeset, :shared_with) + ) do + {:cont, put_flash(socket, :info, "Bounty created")} + else + %{valid?: false} -> + {:cont, assign(socket, :bounty_form, to_form(changeset))} + + {:error, :already_exists} -> + {:cont, put_flash(socket, :warning, "You already have a bounty for this ticket")} + + {:error, _reason} -> + {:cont, put_flash(socket, :error, "Something went wrong")} + end + else + {:cont, + socket + |> assign(:pending_action, {event, unsigned_params}) + |> push_event("open_popup", %{url: socket.assigns.oauth_url})} + end end defp handle_form_toggle_event("open_main_bounty_form", _params, socket) do diff --git a/lib/algora_web/live/swift_bounties_live.ex b/lib/algora_web/live/swift_bounties_live.ex index 32e71469b..c75133a89 100644 --- a/lib/algora_web/live/swift_bounties_live.ex +++ b/lib/algora_web/live/swift_bounties_live.ex @@ -575,7 +575,7 @@ defmodule AlgoraWeb.SwiftBountiesLive do |> redirect(to: AlgoraWeb.UserAuth.generate_login_path(user.email))} {:error, :already_exists} -> - {:noreply, put_flash(socket, :warning, "You have already created a bounty for this ticket")} + {:noreply, put_flash(socket, :warning, "You already have a bounty for this ticket")} {:error, _reason} -> {:noreply, put_flash(socket, :error, "Something went wrong")} From cf134fe4f4d64e25b699766e99aa35721d49b607 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 19:22:45 +0300 Subject: [PATCH 06/20] refactor: streamline bounty form validation and event handling - Updated the bounty form validation to require only the title for custom types, enhancing clarity in user input requirements. - Simplified the HTML structure for the form by removing unnecessary string conversions, improving readability. - Refactored event handling methods to unify the naming convention, ensuring consistency across the LiveView modules. - Initialized the bounty form with a default type of "github" to improve user experience during form creation. --- lib/algora_web/forms/bounty_form.ex | 17 +++++------------ lib/algora_web/live/org/nav.ex | 17 ++++++++++------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/algora_web/forms/bounty_form.ex b/lib/algora_web/forms/bounty_form.ex index 97b27c83c..6766a0024 100644 --- a/lib/algora_web/forms/bounty_form.ex +++ b/lib/algora_web/forms/bounty_form.ex @@ -36,12 +36,13 @@ defmodule AlgoraWeb.Forms.BountyForm do |> validate_type_fields() |> Validations.validate_money_positive(:amount) |> Validations.validate_ticket_ref(:url, :ticket_ref) - |> validate_subset(:type, Enum.map(type_options(), &elem(&1, 1))) + + # |> validate_subset(:type, Enum.map(type_options(), &elem(&1, 1))) end defp validate_type_fields(changeset) do case get_field(changeset, :type) do - "custom" -> validate_required(changeset, [:title, :description]) + :custom -> validate_required(changeset, [:title]) _ -> validate_required(changeset, [:url]) end end @@ -92,10 +93,7 @@ defmodule AlgoraWeb.Forms.BountyForm do <% end %>
-
+
<.input label="URL" field={@form[:url]} @@ -105,12 +103,7 @@ defmodule AlgoraWeb.Forms.BountyForm do
<.input label="Title" field={@form[:title]} placeholder="Brief description of the bounty" /> <.input diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index 9645845cd..bbf804371 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -37,23 +37,25 @@ defmodule AlgoraWeb.Org.Nav do {:cont, socket |> assign(:screenshot?, not is_nil(params["screenshot"])) - |> assign(:main_bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{}))) + |> assign(:main_bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{type: "github"}))) |> assign(:main_bounty_form_open?, false) |> assign(:current_org, current_org) |> assign(:current_user_role, current_user_role) |> assign(:nav, nav_items(current_org.handle, current_user_role)) |> assign(:contacts, contacts) |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3) - |> attach_hook(:form_toggle, :handle_event, &handle_form_toggle_event/3)} + |> attach_hook(:handle_event, :handle_event, &handle_event/3)} end # TODO: handle submit # TODO: handle validate - defp handle_form_toggle_event("create_bounty_main" = event, %{"bounty_form" => params} = unsigned_params, socket) do + defp handle_event("create_bounty_main" = event, %{"bounty_form" => params} = unsigned_params, socket) do + dbg(params) + if socket.assigns.has_fresh_token? do changeset = %BountyForm{} |> BountyForm.changeset(params) |> Map.put(:action, :validate) - + dbg(changeset) amount = get_field(changeset, :amount) ticket_ref = get_field(changeset, :ticket_ref) @@ -77,7 +79,7 @@ defmodule AlgoraWeb.Org.Nav do {:cont, put_flash(socket, :info, "Bounty created")} else %{valid?: false} -> - {:cont, assign(socket, :bounty_form, to_form(changeset))} + {:cont, assign(socket, :main_bounty_form, to_form(changeset))} {:error, :already_exists} -> {:cont, put_flash(socket, :warning, "You already have a bounty for this ticket")} @@ -86,6 +88,7 @@ defmodule AlgoraWeb.Org.Nav do {:cont, put_flash(socket, :error, "Something went wrong")} end else + # TODO: handle pending action {:cont, socket |> assign(:pending_action, {event, unsigned_params}) @@ -93,11 +96,11 @@ defmodule AlgoraWeb.Org.Nav do end end - defp handle_form_toggle_event("open_main_bounty_form", _params, socket) do + defp handle_event("open_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, true)} end - defp handle_form_toggle_event("close_main_bounty_form", _params, socket) do + defp handle_event("close_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, false)} end From dc7a7218b27f978be8a292a6ff2f9720f3590242 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 19:30:59 +0300 Subject: [PATCH 07/20] use atoms --- lib/algora_web/forms/bounty_form.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/algora_web/forms/bounty_form.ex b/lib/algora_web/forms/bounty_form.ex index 6766a0024..61a90c86f 100644 --- a/lib/algora_web/forms/bounty_form.ex +++ b/lib/algora_web/forms/bounty_form.ex @@ -26,7 +26,7 @@ defmodule AlgoraWeb.Forms.BountyForm do end def type_options do - [{"GitHub issue", "github"}, {"Custom", "custom"}] + [{"GitHub issue", :github}, {"Custom", :custom}] end def changeset(form, params) do @@ -73,7 +73,7 @@ defmodule AlgoraWeb.Forms.BountyForm do <.input type="radio" field={@form[:type]} - checked={to_string(get_field(@form.source, :type)) == value} + checked={get_field(@form.source, :type) == value} value={value} class="sr-only" phx-click={ From 646dc65982563e5168aa685e25a167cd424af4ca Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 19:32:39 +0300 Subject: [PATCH 08/20] remove init --- lib/algora_web/live/org/nav.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index bbf804371..99cd7e60c 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -37,7 +37,7 @@ defmodule AlgoraWeb.Org.Nav do {:cont, socket |> assign(:screenshot?, not is_nil(params["screenshot"])) - |> assign(:main_bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{type: "github"}))) + |> assign(:main_bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{}))) |> assign(:main_bounty_form_open?, false) |> assign(:current_org, current_org) |> assign(:current_user_role, current_user_role) From c59dd5b0834367fcfdfa91278377bad673b1c975 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 19:58:43 +0300 Subject: [PATCH 09/20] fix misc issues --- lib/algora_web/components/core_components.ex | 2 +- lib/algora_web/forms/bounty_form.ex | 15 ++--- lib/algora_web/live/org/nav.ex | 66 ++++++++++---------- 3 files changed, 39 insertions(+), 44 deletions(-) diff --git a/lib/algora_web/components/core_components.ex b/lib/algora_web/components/core_components.ex index ea744e339..6741699b0 100644 --- a/lib/algora_web/components/core_components.ex +++ b/lib/algora_web/components/core_components.ex @@ -762,7 +762,7 @@ defmodule AlgoraWeb.CoreComponents do def input(%{type: "radio", value: value} = assigns) do assigns = - assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("radio", value) end) + assign_new(assigns, :checked, fn -> "radio" |> Phoenix.HTML.Form.normalize_value(value) |> dbg() end) ~H"""
diff --git a/lib/algora_web/forms/bounty_form.ex b/lib/algora_web/forms/bounty_form.ex index 61a90c86f..13db156fe 100644 --- a/lib/algora_web/forms/bounty_form.ex +++ b/lib/algora_web/forms/bounty_form.ex @@ -63,7 +63,7 @@ defmodule AlgoraWeb.Forms.BountyForm do } /> --%> -
+
<%= for {label, value} <- type_options() do %>
-
+
<.input label="URL" field={@form[:url]} @@ -101,13 +101,10 @@ defmodule AlgoraWeb.Forms.BountyForm do />
-
+ diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index c933b1f2d..f00e0b209 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -1428,6 +1428,7 @@ defmodule AlgoraWeb.Org.DashboardLive do {@current_org.name}
diff --git a/lib/algora_web/live/user/dashboard_live.ex b/lib/algora_web/live/user/dashboard_live.ex index fcd5b9948..3ed0b0cb1 100644 --- a/lib/algora_web/live/user/dashboard_live.ex +++ b/lib/algora_web/live/user/dashboard_live.ex @@ -300,6 +300,7 @@ defmodule AlgoraWeb.User.DashboardLive do src={~p"/og/@/#{@current_user.handle}"} alt={@current_user.name} class="mt-3 w-full aspect-[1200/630] rounded-lg ring-1 ring-input bg-black" + loading="lazy" />
From 1a78c42ff1c3832c96ca478e40cffee99ebfea77 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 20:26:25 +0300 Subject: [PATCH 12/20] add custom bounty params --- lib/algora/bounties/bounties.ex | 3 ++- lib/algora_web/live/org/nav.ex | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index 2627f4b29..4f107a2b1 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -184,7 +184,8 @@ defmodule Algora.Bounties do creator: User.t(), owner: User.t(), amount: Money.t(), - ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()} + title: String.t(), + description: String.t() }, opts :: [ strategy: strategy(), diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index 589905e8e..da25511b7 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -83,7 +83,9 @@ defmodule AlgoraWeb.Org.Nav do %{ creator: socket.assigns.current_user, owner: socket.assigns.current_org, - amount: data.amount + amount: data.amount, + title: data.title, + description: data.description }, visibility: get_field(changeset, :visibility), shared_with: get_field(changeset, :shared_with) From 9f3d9081c2443808a1b1a8cb1c458f4f3f2a3e91 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 20:37:10 +0300 Subject: [PATCH 13/20] refactor: update bounty notification and ticket schema - Modified the `notify_bounty` function to return `{:ok, nil}` instead of `{:ok, Oban.Job.t()}`, simplifying the return type. - Updated the `Ticket` changeset to require only the `:title`, making the `:url` field optional. - Added a migration to make the `url` field in the `tickets` table nullable, enhancing flexibility in ticket creation. - Refactored the `Nav` module to improve redirection logic after bounty creation based on type, enhancing user experience. --- lib/algora/bounties/bounties.ex | 3 ++- lib/algora/workspace/schemas/ticket.ex | 2 +- lib/algora_web/live/org/nav.ex | 14 ++++++++++++-- .../20250401172828_make_ticket_url_nullable.exs | 9 +++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 priv/repo/migrations/20250401172828_make_ticket_url_nullable.exs diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index 4f107a2b1..fec09a23b 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -410,9 +410,10 @@ defmodule Algora.Bounties do end @spec notify_bounty(%{owner: User.t(), bounty: Bounty.t()}, opts :: []) :: - {:ok, Oban.Job.t()} | {:error, atom()} + {:ok, nil} | {:error, atom()} def notify_bounty(%{owner: _owner, bounty: bounty}, _opts) do Algora.Admin.alert("Notify bounty: #{inspect(bounty)}", :error) + {:ok, nil} end @spec do_claim_bounty(%{ diff --git a/lib/algora/workspace/schemas/ticket.ex b/lib/algora/workspace/schemas/ticket.ex index 4c8e1246c..081279f68 100644 --- a/lib/algora/workspace/schemas/ticket.ex +++ b/lib/algora/workspace/schemas/ticket.ex @@ -32,7 +32,7 @@ defmodule Algora.Workspace.Ticket do def changeset(ticket, params) do ticket |> cast(params, [:title, :description, :url]) - |> validate_required([:title, :url]) + |> validate_required([:title]) |> generate_id() end diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index da25511b7..d814c6f45 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -1,6 +1,7 @@ defmodule AlgoraWeb.Org.Nav do @moduledoc false use Phoenix.Component + use AlgoraWeb, :verified_routes import Ecto.Changeset import Phoenix.LiveView @@ -93,8 +94,17 @@ defmodule AlgoraWeb.Org.Nav do end case bounty_res do - {:ok, _bounty} -> - {:cont, put_flash(socket, :info, "Bounty created")} + {:ok, bounty} -> + to = + case data.type do + :github -> + ~p"/#{data.ticket_ref.owner}/#{data.ticket_ref.repo}/issues/#{data.ticket_ref.number}" + + :custom -> + ~p"/org/#{socket.assigns.current_org.handle}/bounties/#{bounty.id}" + end + + {:cont, redirect(socket, to: to)} {:error, :already_exists} -> {:cont, put_flash(socket, :warning, "You already have a bounty for this ticket")} diff --git a/priv/repo/migrations/20250401172828_make_ticket_url_nullable.exs b/priv/repo/migrations/20250401172828_make_ticket_url_nullable.exs new file mode 100644 index 000000000..f9ac8bc39 --- /dev/null +++ b/priv/repo/migrations/20250401172828_make_ticket_url_nullable.exs @@ -0,0 +1,9 @@ +defmodule Algora.Repo.Migrations.MakeTicketUrlNullable do + use Ecto.Migration + + def change do + alter table(:tickets) do + modify :url, :string, null: true + end + end +end From 7e565497782c5ee3f013a89584d32e6eac7458f0 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 20:43:42 +0300 Subject: [PATCH 14/20] refactor: enhance bounty and OG live modules with host assignment - Introduced a `host` assignment in both `BountyLive` and `OG.BountyLive` modules to streamline access to user data associated with the bounty's ticket repository. - Updated ticket reference handling to conditionally assign values based on the presence of a repository, improving robustness. - Adjusted the rendering logic to utilize the new `host` assignment for displaying user information and avatar, enhancing code clarity and maintainability. --- lib/algora_web/live/bounty_live.ex | 35 ++++++++++++++++++--------- lib/algora_web/live/og/bounty_live.ex | 30 ++++++++++++++--------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index ea9c6874d..55ed196ee 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -6,6 +6,7 @@ defmodule AlgoraWeb.BountyLive do import Ecto.Query alias Algora.Accounts + alias Algora.Accounts.User alias Algora.Admin alias Algora.Bounties alias Algora.Bounties.Bounty @@ -68,15 +69,22 @@ defmodule AlgoraWeb.BountyLive do |> Repo.get!(bounty_id) |> Repo.preload([:owner, :creator, :transactions, ticket: [repository: [:user]]]) - ticket_ref = %{ - owner: bounty.ticket.repository.user.provider_login, - repo: bounty.ticket.repository.name, - number: bounty.ticket.number - } + {host, ticket_ref} = + if bounty.ticket.repository do + {bounty.ticket.repository.user, + %{ + owner: bounty.ticket.repository.user.provider_login, + repo: bounty.ticket.repository.name, + number: bounty.ticket.number + }} + else + {bounty.owner, nil} + end socket |> assign(:bounty, bounty) |> assign(:ticket_ref, ticket_ref) + |> assign(:host, host) |> on_mount(bounty) end @@ -105,6 +113,7 @@ defmodule AlgoraWeb.BountyLive do socket |> assign(:bounty, bounty) |> assign(:ticket_ref, ticket_ref) + |> assign(:host, bounty.ticket.repository.user) |> on_mount(bounty) end @@ -135,9 +144,13 @@ defmodule AlgoraWeb.BountyLive do end share_url = - url( - ~p"/#{socket.assigns.ticket_ref.owner}/#{socket.assigns.ticket_ref.repo}/issues/#{socket.assigns.ticket_ref.number}" - ) + if socket.assigns.ticket_ref do + url( + ~p"/#{socket.assigns.ticket_ref.owner}/#{socket.assigns.ticket_ref.repo}/issues/#{socket.assigns.ticket_ref.number}" + ) + else + url(~p"/org/#{socket.assigns.bounty.owner.handle}/bounties/#{socket.assigns.bounty.id}") + end {:ok, socket @@ -311,9 +324,9 @@ defmodule AlgoraWeb.BountyLive do
<.avatar class="h-12 w-12 sm:h-20 sm:w-20 rounded-lg sm:rounded-2xl"> - <.avatar_image src={@ticket.repository.user.avatar_url} /> + <.avatar_image src={@host.avatar_url} /> <.avatar_fallback> - {Util.initials(@ticket.repository.user.provider_login)} + {Util.initials(User.handle(@host))}
@@ -331,7 +344,7 @@ defmodule AlgoraWeb.BountyLive do rel="noopener" class="block text-base font-display sm:text-xl font-medium text-muted-foreground hover:underline" > - {@ticket.repository.user.provider_login}/{@ticket.repository.name}#{@ticket.number} + {@host.provider_login}/{@ticket.repository.name}#{@ticket.number}
diff --git a/lib/algora_web/live/og/bounty_live.ex b/lib/algora_web/live/og/bounty_live.ex index e854f3bfd..95bbc6f5d 100644 --- a/lib/algora_web/live/og/bounty_live.ex +++ b/lib/algora_web/live/og/bounty_live.ex @@ -11,15 +11,22 @@ defmodule AlgoraWeb.OG.BountyLive do |> Repo.get!(id) |> Repo.preload([:owner, :creator, :transactions, ticket: [repository: [:user]]]) - ticket_ref = %{ - owner: bounty.ticket.repository.user.provider_login, - repo: bounty.ticket.repository.name, - number: bounty.ticket.number - } + {host, ticket_ref} = + if bounty.ticket.repository do + {bounty.ticket.repository.user, + %{ + owner: bounty.ticket.repository.user.provider_login, + repo: bounty.ticket.repository.name, + number: bounty.ticket.number + }} + else + {bounty.owner, nil} + end {:ok, socket |> assign(:bounty, bounty) + |> assign(:host, host) |> assign(:ticket_ref, ticket_ref)} end @@ -29,20 +36,19 @@ defmodule AlgoraWeb.OG.BountyLive do
algora.io
-
+
{@bounty.ticket.repository.name}#{@bounty.ticket.number}
- Algora + Algora

- {@bounty.ticket.repository.user.provider_login} + {@host.provider_login}

From 912e6daf477d56b7375d493bc694a2386f18d41f Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 20:50:25 +0300 Subject: [PATCH 15/20] refactor: clean up bounty form and event handling - Removed commented-out code in the bounty form to enhance readability and maintainability. - Streamlined the `handle_event` function in the `Nav` module by eliminating unnecessary checks and improving the flow of bounty creation logic. - Ensured that the changeset handling is more concise, enhancing clarity in the event processing for bounty creation. --- lib/algora_web/forms/bounty_form.ex | 14 --- lib/algora_web/live/org/nav.ex | 131 +++++++++++++--------------- 2 files changed, 60 insertions(+), 85 deletions(-) diff --git a/lib/algora_web/forms/bounty_form.ex b/lib/algora_web/forms/bounty_form.ex index 13db156fe..a1450c968 100644 --- a/lib/algora_web/forms/bounty_form.ex +++ b/lib/algora_web/forms/bounty_form.ex @@ -36,8 +36,6 @@ defmodule AlgoraWeb.Forms.BountyForm do |> validate_type_fields() |> Validations.validate_money_positive(:amount) |> Validations.validate_ticket_ref(:url, :ticket_ref) - - # |> validate_subset(:type, Enum.map(type_options(), &elem(&1, 1))) end defp validate_type_fields(changeset) do @@ -51,18 +49,6 @@ defmodule AlgoraWeb.Forms.BountyForm do ~H""" <.form id="main-bounty-form" for={@form} phx-submit="create_bounty_main">
- <%!-- <.input type="hidden" name="bounty_form[visibility]" value="exclusive" /> --%> - <%!-- <.input - type="hidden" - name="bounty_form[shared_with][]" - value={ - case @selected_developer do - %User{handle: nil, provider_id: provider_id} -> [to_string(provider_id)] - %User{id: id} -> [id] - end - } - /> --%> -
<%= for {label, value} <- type_options() do %>
@@ -413,6 +415,7 @@ defmodule AlgoraWeb.BountyLive do <%= if @bounty.deadline do %> Expires on {Calendar.strftime(@bounty.deadline, "%b %d, %Y")} <.button + :if={@can_create_bounty} variant="ghost" size="icon-sm" phx-click="exclusive" @@ -425,7 +428,7 @@ defmodule AlgoraWeb.BountyLive do <% else %> @@ -434,9 +437,17 @@ defmodule AlgoraWeb.BountyLive do <% end %>
- <.button variant="secondary" phx-click="exclusive" class="mt-3"> + <.button + :if={@can_create_bounty} + variant="secondary" + phx-click="exclusive" + class="mt-3" + > <.icon name="tabler-user-plus" class="size-5 mr-2 -ml-1" /> Add +
+ Open to everyone +

<%= for user <- @exclusives do %> diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index f00e0b209..ddc20629e 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -16,6 +16,7 @@ defmodule AlgoraWeb.Org.DashboardLive do alias Algora.Contracts alias Algora.Github alias Algora.Organizations + alias Algora.Organizations.Member alias Algora.Payments alias Algora.Payments.Transaction alias Algora.Repo @@ -50,7 +51,7 @@ defmodule AlgoraWeb.Org.DashboardLive do def mount(_params, _session, socket) do %{current_org: current_org} = socket.assigns - if socket.assigns.current_user_role in [:admin, :mod] do + if Member.can_create_bounty?(socket.assigns.current_user_role) do if connected?(socket) do Phoenix.PubSub.subscribe(Algora.PubSub, "auth:#{socket.id}") end diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index 98017a1a3..e7db990bc 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -8,6 +8,7 @@ defmodule AlgoraWeb.Org.Nav do alias Algora.Bounties alias Algora.Organizations + alias Algora.Organizations.Member alias AlgoraWeb.Forms.BountyForm alias AlgoraWeb.OrgAuth @@ -37,10 +38,15 @@ defmodule AlgoraWeb.Org.Nav do # } # end) + main_bounty_form = + if Member.can_create_bounty?(current_user_role) do + to_form(BountyForm.changeset(%BountyForm{}, %{})) + end + {:cont, socket |> assign(:screenshot?, not is_nil(params["screenshot"])) - |> assign(:main_bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{}))) + |> assign(:main_bounty_form, main_bounty_form) |> assign(:main_bounty_form_open?, false) |> assign(:current_org, current_org) |> assign(:current_user_role, current_user_role) @@ -50,7 +56,7 @@ defmodule AlgoraWeb.Org.Nav do |> attach_hook(:handle_event, :handle_event, &handle_event/3)} end - defp handle_event("create_bounty_main" = event, %{"bounty_form" => params}, socket) do + defp handle_event("create_bounty_main", %{"bounty_form" => params}, socket) do changeset = BountyForm.changeset(%BountyForm{}, params) case apply_action(changeset, :save) do @@ -123,6 +129,10 @@ defmodule AlgoraWeb.Org.Nav do {:cont, assign(socket, :main_bounty_form_open?, false)} end + defp handle_event(_event, _params, socket) do + {:cont, socket} + end + defp handle_active_tab_params(_params, _url, socket) do active_tab = case {socket.view, socket.assigns.live_action} do From 33b5e7b0ff231f5e200dce98cc23a8a30d1c6be7 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 21:18:31 +0300 Subject: [PATCH 17/20] refactor: improve bounty ordering logic in BountyLive - Updated the ordering of bounties in the `BountyLive` module to prioritize the current organization, enhancing the relevance of displayed bounties. - Removed a TODO comment regarding pooling bounties for better code clarity. --- lib/algora_web/live/bounty_live.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/algora_web/live/bounty_live.ex b/lib/algora_web/live/bounty_live.ex index 69a5bb215..9023e86a9 100644 --- a/lib/algora_web/live/bounty_live.ex +++ b/lib/algora_web/live/bounty_live.ex @@ -104,9 +104,8 @@ defmodule AlgoraWeb.BountyLive do where: u.provider_login == ^repo_owner, where: r.name == ^repo_name, where: t.number == ^number, - # TODO: pool bounties - limit: 1, - order_by: [asc: b.inserted_at] + order_by: fragment("CASE WHEN ? = ? THEN 0 ELSE 1 END", u.id, ^socket.assigns.current_org.id), + limit: 1 ) |> Repo.one() |> Repo.preload([:owner, :creator, :transactions, ticket: [repository: [:user]]]) From d02873878f789aa5cd8a4a63370206d5fdd4eeb2 Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 21:22:05 +0300 Subject: [PATCH 18/20] refactor: streamline bounty form handling and event logic - Simplified the `input` function in `CoreComponents` by removing unnecessary debugging code. - Enhanced the `RepoNav` module by introducing a conditional `main_bounty_form` assignment based on user permissions, improving clarity and maintainability. - Refactored event handling for bounty creation, ensuring better error handling and user feedback during the process. --- lib/algora_web/components/core_components.ex | 2 +- lib/algora_web/live/org/nav.ex | 2 - lib/algora_web/live/org/repo_nav.ex | 81 ++++++++++++++++++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/lib/algora_web/components/core_components.ex b/lib/algora_web/components/core_components.ex index 6741699b0..ea744e339 100644 --- a/lib/algora_web/components/core_components.ex +++ b/lib/algora_web/components/core_components.ex @@ -762,7 +762,7 @@ defmodule AlgoraWeb.CoreComponents do def input(%{type: "radio", value: value} = assigns) do assigns = - assign_new(assigns, :checked, fn -> "radio" |> Phoenix.HTML.Form.normalize_value(value) |> dbg() end) + assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("radio", value) end) ~H"""
diff --git a/lib/algora_web/live/org/nav.ex b/lib/algora_web/live/org/nav.ex index e7db990bc..5251da856 100644 --- a/lib/algora_web/live/org/nav.ex +++ b/lib/algora_web/live/org/nav.ex @@ -61,8 +61,6 @@ defmodule AlgoraWeb.Org.Nav do case apply_action(changeset, :save) do {:ok, data} -> - dbg(data) - bounty_res = case data.type do :github -> diff --git a/lib/algora_web/live/org/repo_nav.ex b/lib/algora_web/live/org/repo_nav.ex index ca7dddcd5..18ec0311e 100644 --- a/lib/algora_web/live/org/repo_nav.ex +++ b/lib/algora_web/live/org/repo_nav.ex @@ -13,30 +13,99 @@ defmodule AlgoraWeb.Org.RepoNav do current_org = Organizations.get_org_by(provider_login: repo_owner, provider: "github") current_user_role = OrgAuth.get_user_role(current_user, current_org) + main_bounty_form = + if Member.can_create_bounty?(current_user_role) do + to_form(BountyForm.changeset(%BountyForm{}, %{})) + end + {:cont, socket |> assign(:screenshot?, not is_nil(params["screenshot"])) - |> assign(:main_bounty_form, to_form(BountyForm.changeset(%BountyForm{}, %{}))) + |> assign(:main_bounty_form, main_bounty_form) |> assign(:main_bounty_form_open?, false) |> assign(:current_org, current_org) |> assign(:current_user_role, current_user_role) |> assign(:nav, nav_items(current_org.handle, current_user_role)) |> assign(:contacts, []) |> attach_hook(:active_tab, :handle_params, &handle_active_tab_params/3) - |> attach_hook(:form_toggle, :handle_event, &handle_form_toggle_event/3)} + |> attach_hook(:handle_event, :handle_event, &handle_event/3)} end - # TODO: handle submit - # TODO: handle validate + defp handle_event("create_bounty_main", %{"bounty_form" => params}, socket) do + changeset = BountyForm.changeset(%BountyForm{}, params) + + case apply_action(changeset, :save) do + {:ok, data} -> + bounty_res = + case data.type do + :github -> + Bounties.create_bounty( + %{ + creator: socket.assigns.current_user, + owner: socket.assigns.current_org, + amount: data.amount, + ticket_ref: %{ + owner: data.ticket_ref.owner, + repo: data.ticket_ref.repo, + number: data.ticket_ref.number + } + }, + visibility: get_field(changeset, :visibility), + shared_with: get_field(changeset, :shared_with) + ) + + :custom -> + Bounties.create_bounty( + %{ + creator: socket.assigns.current_user, + owner: socket.assigns.current_org, + amount: data.amount, + title: data.title, + description: data.description + }, + visibility: get_field(changeset, :visibility), + shared_with: get_field(changeset, :shared_with) + ) + end + + case bounty_res do + {:ok, bounty} -> + to = + case data.type do + :github -> + ~p"/#{data.ticket_ref.owner}/#{data.ticket_ref.repo}/issues/#{data.ticket_ref.number}" + + :custom -> + ~p"/org/#{socket.assigns.current_org.handle}/bounties/#{bounty.id}" + end + + {:cont, redirect(socket, to: to)} - defp handle_form_toggle_event("open_main_bounty_form", _params, socket) do + {:error, :already_exists} -> + {:cont, put_flash(socket, :warning, "You already have a bounty for this ticket")} + + {:error, reason} -> + Logger.error("Failed to create bounty: #{inspect(reason)}") + {:cont, put_flash(socket, :error, "Something went wrong")} + end + + {:error, changeset} -> + {:cont, assign(socket, :main_bounty_form, to_form(changeset))} + end + end + + defp handle_event("open_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, true)} end - defp handle_form_toggle_event("close_main_bounty_form", _params, socket) do + defp handle_event("close_main_bounty_form", _params, socket) do {:cont, assign(socket, :main_bounty_form_open?, false)} end + defp handle_event(_event, _params, socket) do + {:cont, socket} + end + defp handle_active_tab_params(_params, _url, socket) do active_tab = case {socket.view, socket.assigns.live_action} do From be8ee005f2031ce6459d75c7b56605f787c55cad Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 21:27:22 +0300 Subject: [PATCH 19/20] refactor: simplify bounty form structure and enhance UI responsiveness - Removed redundant radio button options for bounty form type, streamlining the user interface. - Consolidated input fields for bounty details, improving layout and accessibility. - Updated the share drawer component to maintain functionality while enhancing code clarity. --- lib/algora_web/live/org/dashboard_live.ex | 105 ++++------------------ lib/algora_web/live/org/repo_nav.ex | 6 ++ 2 files changed, 22 insertions(+), 89 deletions(-) diff --git a/lib/algora_web/live/org/dashboard_live.ex b/lib/algora_web/live/org/dashboard_live.ex index ddc20629e..413c1b6ce 100644 --- a/lib/algora_web/live/org/dashboard_live.ex +++ b/lib/algora_web/live/org/dashboard_live.ex @@ -90,8 +90,7 @@ defmodule AlgoraWeb.Org.DashboardLive do # Will be initialized when chat starts |> assign(:thread, nil) |> assign(:messages, []) - |> assign(:show_chat, false) - |> assign(:bounty_form_type, "github")} + |> assign(:show_chat, false)} else {:ok, redirect(socket, to: ~p"/org/#{current_org.handle}/home")} end @@ -1665,89 +1664,24 @@ defmodule AlgoraWeb.Org.DashboardLive do end } /> - -
- - -
- -
- <.input - label="URL" - field={@bounty_form[:url]} - placeholder="https://github.com/owner/repo/issues/123" - /> -
- -
- <.input - label="Title" - field={@bounty_form[:title]} - placeholder="Brief description of the bounty" - /> - <.input - label="Description" - field={@bounty_form[:description]} - type="textarea" - placeholder="Requirements and acceptance criteria" - /> -
- + <.input + label="URL" + field={@bounty_form[:url]} + placeholder="https://github.com/owner/repo/issues/123" + /> <.input label="Amount" icon="tabler-currency-dollar" field={@bounty_form[:amount]} />
-
- <.button variant="secondary" phx-click="close_share_drawer" type="button"> - Cancel - - <.button type="submit"> - Share Bounty <.icon name="tabler-arrow-right" class="-mr-1 ml-2 h-4 w-4" /> - -
+ +
+ <.button variant="secondary" phx-click="close_share_drawer" type="button"> + Cancel + + <.button type="submit"> + Share Bounty <.icon name="tabler-arrow-right" class="-mr-1 ml-2 h-4 w-4" /> + +
""" end @@ -1863,12 +1797,7 @@ defmodule AlgoraWeb.Org.DashboardLive do defp share_drawer(assigns) do ~H""" - <.drawer - show={@show_share_drawer} - direction="bottom" - on_cancel="close_share_drawer" - class={if @share_drawer_type == "bounty", do: "h-[100svh]"} - > + <.drawer show={@show_share_drawer} direction="bottom" on_cancel="close_share_drawer"> <.share_drawer_header :if={@selected_developer} selected_developer={@selected_developer} @@ -1888,7 +1817,6 @@ defmodule AlgoraWeb.Org.DashboardLive do bounty_form={@bounty_form} tip_form={@tip_form} contract_form={@contract_form} - bounty_form_type={@bounty_form_type} />
<.alert @@ -1912,7 +1840,6 @@ defmodule AlgoraWeb.Org.DashboardLive do bounty_form={@bounty_form} tip_form={@tip_form} contract_form={@contract_form} - bounty_form_type={@bounty_form_type} /> <% end %>
diff --git a/lib/algora_web/live/org/repo_nav.ex b/lib/algora_web/live/org/repo_nav.ex index 18ec0311e..92dcaedaf 100644 --- a/lib/algora_web/live/org/repo_nav.ex +++ b/lib/algora_web/live/org/repo_nav.ex @@ -1,13 +1,19 @@ defmodule AlgoraWeb.Org.RepoNav do @moduledoc false use Phoenix.Component + use AlgoraWeb, :verified_routes + import Ecto.Changeset import Phoenix.LiveView + alias Algora.Bounties alias Algora.Organizations + alias Algora.Organizations.Member alias AlgoraWeb.Forms.BountyForm alias AlgoraWeb.OrgAuth + require Logger + def on_mount(:default, %{"repo_owner" => repo_owner} = params, _session, socket) do current_user = socket.assigns[:current_user] current_org = Organizations.get_org_by(provider_login: repo_owner, provider: "github") From 6a792a52321bafc64b5c773d83a623ce44d42adf Mon Sep 17 00:00:00 2001 From: zafer Date: Tue, 1 Apr 2025 21:32:59 +0300 Subject: [PATCH 20/20] add id to radio input --- lib/algora_web/forms/bounty_form.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/algora_web/forms/bounty_form.ex b/lib/algora_web/forms/bounty_form.ex index a1450c968..52c1b14c5 100644 --- a/lib/algora_web/forms/bounty_form.ex +++ b/lib/algora_web/forms/bounty_form.ex @@ -57,6 +57,7 @@ defmodule AlgoraWeb.Forms.BountyForm do "border-border has-[:checked]:border-primary has-[:checked]:bg-primary/10" ]}> <.input + id={"main-bounty-form-type-#{value}"} type="radio" field={@form[:type]} checked={@form[:type].value == value}