From 80b5cbff911e70bbddae4cc1352518d02f5a4265 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 8 Oct 2026 15:13:12 +0100 Subject: [PATCH 01/25] Move export into Sync tab on project settings Co-Authored-By: Claude Sonnet 5.5 --- .../live/project_live/settings.html.heex | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index fa4fed9806..e0115e79e7 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -54,7 +54,7 @@ <:tab hash="vcs"> <.icon name="hero-arrow-path" class="h-5 w-5 inline-block mr-2" /> - Sync to GitHub + Sync <:tab hash="data-storage"> <.icon name="hero-square-3-stack-3d" class="h-5 w-5 inline-block mr-2" /> @@ -249,21 +249,6 @@ current_user={@current_user} /> -
-
-
Export your Project
-

- Export your project as code, to save this version or edit your project locally. -

-
- <.button_link - theme="primary" - href={~p"/download/yaml?id=#{@project.id}"} - target="_blank" - > - Export project - -
<%= if @can_delete_project do %>
@@ -757,8 +742,8 @@ <:panel hash="vcs" class="space-y-4"> <.section_header - title="Version control" - subtitle="View or modify external version control settings for this project." + title="Sync" + subtitle="Sync your project to version control or other environments from here." permissions_message={"version control configuration#{@can_initiate_github_sync && " but you can initiate a sync." || "."}"} can_perform_action={@can_install_github} /> @@ -767,6 +752,22 @@ id="sandbox-banner-vcs" variant={:local} /> +
+ <.icon + name="hero-information-circle" + class="h-5 w-5 shrink-0 text-blue-500" + /> +

+ Learn more about synchronizing your project with GitHub at + <.link + target="_blank" + href="https://docs.openfn.org/documentation/link-to-GitHub" + class="font-medium underline" + > + docs.openfn.org/documentation/link-to-GitHub + +

+
<%= if assigns[:github_banner] do %> {Phoenix.LiveView.TagEngine.component( @@ -839,24 +840,27 @@ <% else %>
- Sync to GitHub + Connect to GitHub
Version Control is not configured for this Lightning instance. Contact the superuser for more information.
<% end %> -
- Need to learn more about GitHub Sync? - See - <.link +
+
+
Export your Project
+

+ Save your project as a YAML file to be edited locally, backed up, or deployed elsewhere. +

+
+ <.button_link + theme="primary" + href={~p"/download/yaml?id=#{@project.id}"} target="_blank" - href="https://docs.openfn.org/documentation/deploy/portability#automated-version-control-with-github-and-lightning" - class="link" > - portability docs - - for full documentation on associated GitHub actions and automated workflows. + Export project +
<:panel hash="data-storage" class="space-y-4"> From 547f8cce501d623568d6b01362831d6941d18d50 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 8 Oct 2026 15:38:45 +0100 Subject: [PATCH 02/25] Add CLI section to Sync tab Co-Authored-By: Claude Sonnet 5.5 --- .../live/project_live/settings.html.heex | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index e0115e79e7..70be78b5b1 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -764,7 +764,7 @@ href="https://docs.openfn.org/documentation/link-to-GitHub" class="font-medium underline" > - docs.openfn.org/documentation/link-to-GitHub + docs.openfn.org

@@ -862,6 +862,44 @@ Export project
+
+
+
Sync with CLI
+

+ Use the CLI to push and pull this project locally +

+
+
+
+

+ Pull this project into a local workspace: +

+
openfn project pull {@project.id}
+
+
+

+ Push local changes back to this project: +

+
openfn project deploy
+
+
+

+ Run a workflow: +

+
openfn <workflow-name>
+
+
+

+ Learn more in the + <.link + target="_blank" + href="https://docs.openfn.org/documentation/sync" + class="font-medium text-indigo-600 hover:text-indigo-900" + > + CLI Sync Docs + +

+
<:panel hash="data-storage" class="space-y-4"> <.section_header From 9e3bd1133a1e2436b2673388522afdf756258f41 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 8 Oct 2026 16:42:32 +0100 Subject: [PATCH 03/25] Refine GitHub sync form layout, copy and searchable selects Co-Authored-By: Claude Sonnet 5.5 --- assets/js/hooks/SearchableSelect.ts | 59 ++++++ assets/js/hooks/index.ts | 2 + lib/lightning_web/components/new_inputs.ex | 22 +++ .../project_live/github_sync_component.ex | 16 +- .../github_sync_component.html.heex | 181 +++++++++--------- .../live/project_live/settings.html.heex | 3 +- 6 files changed, 184 insertions(+), 99 deletions(-) create mode 100644 assets/js/hooks/SearchableSelect.ts diff --git a/assets/js/hooks/SearchableSelect.ts b/assets/js/hooks/SearchableSelect.ts new file mode 100644 index 0000000000..adbea63e8e --- /dev/null +++ b/assets/js/hooks/SearchableSelect.ts @@ -0,0 +1,59 @@ +import type { PhoenixHook } from './PhoenixHook'; + +type SearchableSelect = PhoenixHook<{ + search: HTMLInputElement | null; + applyFilter(): void; +}>; + +/** + * Adds a text filter to the `custom-select` input (`searchable` attr). + * + * Options are filtered client-side by hiding non-matching `li[role=option]` + * elements. The filter is re-applied after every LiveView patch so a re-render + * of the options doesn't leave the list out of sync with the search box. + */ +const SearchableSelect = { + mounted() { + this.search = this.el.querySelector( + 'input[data-select-search]' + ); + + if (!this.search) return; + + this.search.addEventListener('input', () => this.applyFilter()); + + this.search.addEventListener('keydown', e => { + // Don't submit the surrounding form. + if (e.key === 'Enter') { + e.preventDefault(); + this.el + .querySelector('li[role=option]:not([hidden])') + ?.click(); + } + }); + + // The wrapper's phx-click opens the list; focus the search box and start + // with a clean filter whenever the toggle button is clicked. + this.el.querySelector('button')?.addEventListener('click', () => { + requestAnimationFrame(() => { + if (!this.search) return; + this.search.value = ''; + this.applyFilter(); + this.search.focus(); + }); + }); + }, + updated() { + this.applyFilter(); + }, + applyFilter() { + const term = (this.search?.value ?? '').trim().toLowerCase(); + + this.el.querySelectorAll('li[role=option]').forEach(option => { + const label = (option.textContent ?? '').toLowerCase(); + option.hidden = term !== '' && !label.includes(term); + }); + }, +} as SearchableSelect; + +export { SearchableSelect }; diff --git a/assets/js/hooks/index.ts b/assets/js/hooks/index.ts index 67f5407788..4bb524d0c5 100644 --- a/assets/js/hooks/index.ts +++ b/assets/js/hooks/index.ts @@ -25,6 +25,7 @@ import { } from './KeyHandlers'; import LogLineHighlight from './LogLineHighlight'; import type { PhoenixHook } from './PhoenixHook'; +import { SearchableSelect } from './SearchableSelect'; import { TabbedContainer } from './TabbedContainer'; export { @@ -34,6 +35,7 @@ export { TemplateToWorkflow, ElapsedIndicator, TabbedContainer, + SearchableSelect, SaveViaCtrlS, InspectorSaveViaCtrlS, OpenSyncModalViaCtrlShiftS, diff --git a/lib/lightning_web/components/new_inputs.ex b/lib/lightning_web/components/new_inputs.ex index 1cef631f16..4d9d47e43b 100644 --- a/lib/lightning_web/components/new_inputs.ex +++ b/lib/lightning_web/components/new_inputs.ex @@ -398,6 +398,10 @@ defmodule LightningWeb.Components.NewInputs do attr :button_placement, :string, default: nil + attr :searchable, :boolean, + default: false, + doc: "custom-select only: show a text box that filters the options" + attr :placeholder, :string, default: "" attr :autocomplete, :string, default: "off" @@ -524,7 +528,9 @@ defmodule LightningWeb.Components.NewInputs do

- This option will commit a copy of your current OpenFn project to a GitHub repo. + This option will commit a copy of your current OpenFn project to GitHub. It does not affect any workflows or configuration here.

@@ -825,10 +822,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do Import from GitHub (overwrite this project)

- If you already have an openfn.yaml - (or legacy config.json) - tracked on GitHub and you want to overwrite - this project on OpenFn, you can choose this advanced option. + This option will replace this project with the workflows, collections and channels that are already committed to GitHub.

@@ -886,7 +880,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do <% end %> - Existing versions of these files on these branches will be overwritten. (I'll be able to find them in my git history if needed.) + Existing versions of these files on these branches will be overwritten (but will still exist in git history). """ diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index 3c88963c7f..3b3ada2094 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -9,6 +9,9 @@ phx-submit="save" phx-target={@myself} > +
+ Configure Two-Way Sync to GitHub +
<%!-- Token is expired/invalid --%> <.error_banner :if={show_invalid_oauth_error?(@installations)} @@ -56,93 +59,99 @@ There was a problem connecting to GitHub. Refresh the page or contact support if the problem persists.

-
- <.input - id="select-installations-input" - type="custom-select" - field={f[:github_installation_id]} - label="GitHub Installation" - prompt="Select an installation" - options={installations_select_options(@installations)} - disabled={ - @actions_disabled || - if(!@installations.ok?, do: true, else: false) - } - required="true" - button_placement="right" - > - <.button - id="refresh-installation-button" - type="button" - theme="custom" - class="rounded-l-none my-px h-9 text-gray-900 ring-1 ring-gray-300 hover:bg-gray-100" - disabled={ - @actions_disabled || - if(@installations.loading, do: true, else: false) - } - phx-click="refresh-installations" - phx-target={@myself} - > - <.icon - name="hero-arrow-path-mini" - class={"-ml-0.5 h-5 w-5 #{if(@installations.loading, do: "animate-spin")}"} +
+
+
+ <.input + id="select-installations-input" + type="custom-select" + field={f[:github_installation_id]} + label="GitHub Installation" + prompt="Select an installation" + options={installations_select_options(@installations)} + disabled={ + @actions_disabled || + if(!@installations.ok?, do: true, else: false) + } + required="true" + button_placement="right" + > + <.button + id="refresh-installation-button" + type="button" + theme="custom" + class="rounded-l-none my-px h-9 text-gray-900 ring-1 ring-gray-300 hover:bg-gray-100" + disabled={ + @actions_disabled || + if(@installations.loading, do: true, else: false) + } + phx-click="refresh-installations" + phx-target={@myself} + > + <.icon + name="hero-arrow-path-mini" + class={"-ml-0.5 h-5 w-5 #{if(@installations.loading, do: "animate-spin")}"} + /> + + +
+
+ <.input + id="select-repos-input" + searchable + type="custom-select" + field={f[:repo]} + label="Repository" + prompt="Select a repository" + options={ + repos_select_options(@repos, f[:github_installation_id].value) + } + disabled={ + to_string(f[:github_installation_id].value) == "" or !@repos.ok? + } + required="true" /> - - -
-
- Can't find the right installation or repository? - <.link - target="_blank" - class="link" - href={"https://github.com/apps/#{github_config()[:app_name]}"} - > - Create/update GitHub installations or modify permissions. - -
-
- <.input - id="select-repos-input" - type="custom-select" - field={f[:repo]} - label="Repository" - prompt="Select a repository" - options={ - repos_select_options(@repos, f[:github_installation_id].value) - } - disabled={ - to_string(f[:github_installation_id].value) == "" or !@repos.ok? - } - required="true" - /> -
-
- <.input - id="select-branches-input" - type="custom-select" - field={f[:branch]} - label="Branch" - prompt="Select a branch" - options={branches_select_options(@branches, f[:repo].value)} - disabled={to_string(f[:repo].value) == "" or !@branches.ok?} - required="true" - button_placement="right" - > - <.button - id="refresh-branches-button" - type="button" - theme="custom" - class="rounded-l-none my-px h-9 text-gray-900 ring-1 ring-gray-300 hover:bg-gray-100" - disabled={if(@branches.loading, do: true, else: false)} - phx-click="refresh-branches" - phx-target={@myself} - > - <.icon - name="hero-arrow-path-mini" - class={"-ml-0.5 h-5 w-5 #{if(@installations.loading, do: "animate-spin")}"} - /> - - +
+
+ Can't find the right installation or repository? + <.link + target="_blank" + class="link" + href={"https://github.com/apps/#{github_config()[:app_name]}"} + > + Create/update GitHub installations or modify permissions. + +
+
+ <.input + id="select-branches-input" + searchable + type="custom-select" + field={f[:branch]} + label="Branch" + prompt="Select a branch" + options={branches_select_options(@branches, f[:repo].value)} + disabled={to_string(f[:repo].value) == "" or !@branches.ok?} + required="true" + button_placement="right" + > + <.button + id="refresh-branches-button" + type="button" + theme="custom" + class="rounded-l-none my-px h-9 text-gray-900 ring-1 ring-gray-300 hover:bg-gray-100" + disabled={if(@branches.loading, do: true, else: false)} + phx-click="refresh-branches" + phx-target={@myself} + > + <.icon + name="hero-arrow-path-mini" + class={"-ml-0.5 h-5 w-5 #{if(@installations.loading, do: "animate-spin")}"} + /> + + +
+
<.sync_order_radio form={f} /> diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index 70be78b5b1..fbd0e9279d 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -824,8 +824,7 @@ Connect your OpenFn account to GitHub - To create a new GitHub version control connection you must first connect your OpenFn account to GitHub. - Please click the button below to get started. + Connect your OpenFn account to GitHub. Click the button below to get started.
Date: Thu, 8 Oct 2026 16:59:36 +0100 Subject: [PATCH 04/25] Default GitHub sync to v2 YAML with a legacy format switch Co-Authored-By: Claude Sonnet 5.5 --- .../project_live/github_sync_component.ex | 82 +++++++++++++------ .../github_sync_component.html.heex | 11 +-- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 8592e04b38..c64b4a0f25 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -91,6 +91,24 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do end end + def handle_event("toggle-legacy-format", _params, socket) do + changeset = socket.assigns.changeset + current = Ecto.Changeset.get_field(changeset, :sync_version) + + # Form fields read from the changeset's params before its changes, so the + # flip has to go through the params, like the "validate" event does. + params = + (changeset.params || %{}) + |> Map.put("project_id", socket.assigns.project.id) + |> Map.put("sync_version", to_string(!current)) + + {:noreply, + assign(socket, + changeset: + validate_changes(socket.assigns.project_repo_connection, params) + )} + end + def handle_event("refresh-installations", _params, socket) do changeset = validate_changes(socket.assigns.project_repo_connection, %{}) user = socket.assigns.user @@ -120,7 +138,11 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do }) do socket |> assign( - changeset: ProjectRepoConnection.configure_changeset(repo_connection, %{}) + # New connections default to the v2 YAML format. + changeset: + ProjectRepoConnection.configure_changeset(repo_connection, %{ + sync_version: true + }) ) |> assign_async([:installations, :repos], fn -> # repos are grouped using the installation_id @@ -729,39 +751,45 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do attr :form, :map, required: true attr :project_id, :string, required: true + attr :myself, :any, required: true + # Inverted: sync_version is true (v2) unless the legacy switch is on. The + # hidden input carries the current value with the form's phx-change events. defp config_format_toggle(assigns) do + assigns = assign(assigns, :legacy?, !sync_version?(assigns.form)) + ~H""" -
+
+ - + + Use legacy YAML config format +
""" end diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index 3b3ada2094..39728400ef 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -165,11 +165,6 @@ class="placeholder:italic placeholder:text-slate-400" />
- <.config_format_toggle - :if={f[:sync_direction].value != :deploy} - form={f} - project_id={@project.id} - /> <%= if f[:branch].value do %> <.accept_checkbox project={@project} @@ -194,6 +189,12 @@ Connect Branch & Initiate First Sync
+ <.config_format_toggle + :if={f[:sync_direction].value != :deploy} + form={f} + project_id={@project.id} + myself={@myself} + />
From f7419100eddf645bad34ee2c1f31d1a243778c6c Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 8 Oct 2026 17:04:47 +0100 Subject: [PATCH 05/25] Move GitHub docs banner into the connection form Co-Authored-By: Claude Sonnet 5.5 --- .../project_live/github_sync_component.html.heex | 16 ++++++++++++++++ .../live/project_live/settings.html.heex | 16 ---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index 39728400ef..2c393b487c 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -12,6 +12,22 @@
Configure Two-Way Sync to GitHub
+
+ <.icon + name="hero-information-circle" + class="h-5 w-5 shrink-0 text-blue-500" + /> +

+ Learn more about synchronizing your project with GitHub at + <.link + target="_blank" + href="https://docs.openfn.org/documentation/link-to-GitHub" + class="font-medium underline" + > + docs.openfn.org + +

+
<%!-- Token is expired/invalid --%> <.error_banner :if={show_invalid_oauth_error?(@installations)} diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index fbd0e9279d..c357f0df49 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -752,22 +752,6 @@ id="sandbox-banner-vcs" variant={:local} /> -
- <.icon - name="hero-information-circle" - class="h-5 w-5 shrink-0 text-blue-500" - /> -

- Learn more about synchronizing your project with GitHub at - <.link - target="_blank" - href="https://docs.openfn.org/documentation/link-to-GitHub" - class="font-medium underline" - > - docs.openfn.org - -

-
<%= if assigns[:github_banner] do %> {Phoenix.LiveView.TagEngine.component( From 2c97e07d12e1c852ffe4b6408c3af3e17c5515ca Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 8 Oct 2026 17:09:06 +0100 Subject: [PATCH 06/25] Tidy the connected GitHub sync panel Co-Authored-By: Claude Sonnet 5.5 --- .../project_live/github_sync_component.ex | 2 +- .../github_sync_component.html.heex | 70 +++++++++---------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index c64b4a0f25..47508a2fdc 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -531,7 +531,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do defp verify_connection_banner(assigns) do ~H""" -
+
<.async_result assign={@verify_connection}> <:loading>
diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index 2c393b487c..e2db2a4255 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -214,22 +214,23 @@
- <.verify_connection_banner - id="verify-connection-banner" - verify_connection={@verify_connection} - myself={@myself} - changeset={@changeset} - repos={@repos} - project={@project} - can_reconnect={ - @can_install_github && - can_access_github_installation?( - @project_repo_connection, - @installations - ) - } - />
+
Connected to GitHub
+ <.verify_connection_banner + id="verify-connection-banner" + verify_connection={@verify_connection} + myself={@myself} + changeset={@changeset} + repos={@repos} + project={@project} + can_reconnect={ + @can_install_github && + can_access_github_installation?( + @project_repo_connection, + @installations + ) + } + />
Repository: @@ -256,23 +257,21 @@ - + Path to config: {ProjectRepoConnection.config_path(@project_repo_connection)} - - Sync version: - - {if @project_repo_connection.sync_version, - do: "v2 (new)", - else: "v1 (legacy)"} - - +

+ This connection uses the legacy sync format (config.json). Reconnect to update to v2 sync formats +

-
+
<.button id="initiate-sync-button" disabled={ @@ -292,20 +291,19 @@ > Initiate Sync to Branch + <.button + :if={@can_install_github} + id="remove-integration-button" + type="button" + theme="danger" + phx-click={show_modal("remove_connection_modal")} + > + Remove integration +
-
-
- No longer need to sync this project? - <.link - href="#" - class="link link-danger" - phx-click={show_modal("remove_connection_modal")} - phx-target={@myself} - > - Remove Integration. - <.confirm_connection_removal_modal + :if={@can_install_github} id="remove_connection_modal" myself={@myself} /> From 1ac7f1404f0d285d0823b70c8b316f0ce14c97b3 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Fri, 9 Oct 2026 09:59:52 +0100 Subject: [PATCH 07/25] Update settings tests for the Sync tab changes Co-Authored-By: Claude Sonnet 5.5 --- test/lightning_web/live/project_live_test.exs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/lightning_web/live/project_live_test.exs b/test/lightning_web/live/project_live_test.exs index d7a3cfb350..f0cb68d678 100644 --- a/test/lightning_web/live/project_live_test.exs +++ b/test/lightning_web/live/project_live_test.exs @@ -198,7 +198,7 @@ defmodule LightningWeb.ProjectLiveTest do {:ok, index_live, html} = live(conn, ~p"/projects/#{project.id}/settings") assert html =~ - "Export your project as code, to save this version or edit your project locally" + "Save your project as a YAML file to be edited locally, backed up, or deployed elsewhere" assert index_live |> element(~s{a[target="_blank"]}, "Export project") @@ -5076,7 +5076,7 @@ defmodule LightningWeb.ProjectLiveTest do view |> element("#select-repos-input") |> render_async() - |> find_selected_option("#select-repos-input li") + |> find_selected_option("#select-repos-input li[role=option]") assert selected_repo =~ expected_repo["full_name"] @@ -5095,7 +5095,7 @@ defmodule LightningWeb.ProjectLiveTest do view |> element("#select-branches-input") |> render_async() - |> find_selected_option("#select-branches-input li") + |> find_selected_option("#select-branches-input li[role=option]") assert selected_branch =~ expected_branch["name"] @@ -5106,9 +5106,9 @@ defmodule LightningWeb.ProjectLiveTest do html = render_async(view) - refute find_selected_option(html, "#select-repos-input li") + refute find_selected_option(html, "#select-repos-input li[role=option]") - refute find_selected_option(html, "#select-branches-input li") + refute find_selected_option(html, "#select-branches-input li[role=option]") # let us list the branches again by following the ritual again view @@ -5141,7 +5141,7 @@ defmodule LightningWeb.ProjectLiveTest do |> element("#select-branches-input") |> render_async() |> Floki.parse_fragment!() - |> Floki.find("#select-branches-input li") + |> Floki.find("#select-branches-input li[role=option]") assert Enum.count(options) == 2 @@ -5168,7 +5168,7 @@ defmodule LightningWeb.ProjectLiveTest do |> element("#select-branches-input") |> render_async() |> Floki.parse_fragment!() - |> Floki.find("#select-branches-input li") + |> Floki.find("#select-branches-input li[role=option]") assert Enum.count(options) == 3 end @@ -5239,7 +5239,7 @@ defmodule LightningWeb.ProjectLiveTest do |> element("#select-branches-input") |> render_async() |> Floki.parse_fragment!() - |> Floki.find("#select-branches-input li") + |> Floki.find("#select-branches-input li[role=option]") # Only the "Select a branch" prompt is present; no branches leaked. assert Enum.count(options) == 1 @@ -5257,7 +5257,7 @@ defmodule LightningWeb.ProjectLiveTest do |> element("#select-branches-input") |> render_async() |> Floki.parse_fragment!() - |> Floki.find("#select-branches-input li") + |> Floki.find("#select-branches-input li[role=option]") assert Enum.count(options) == 1 end @@ -5317,7 +5317,7 @@ defmodule LightningWeb.ProjectLiveTest do # We just have the default option floki_fragment = view |> render_async() |> Floki.parse_fragment!() [repos_input] = Floki.find(floki_fragment, "#select-repos-input") - options = Floki.children(repos_input) + options = Floki.find(repos_input, "li[role=option]") assert Enum.count(options) == 1 options |> hd() |> Floki.raw_html() =~ "Select a repo" @@ -5331,7 +5331,7 @@ defmodule LightningWeb.ProjectLiveTest do # we should now have the repos listed floki_fragment = view |> render_async() |> Floki.parse_fragment!() [repos_input] = Floki.find(floki_fragment, "#select-repos-input") - options = Floki.children(repos_input) + options = Floki.find(repos_input, "li[role=option]") assert Enum.count(options) == 2 [default_option, repo_option] = options Floki.raw_html(default_option) =~ "Select a repo" @@ -5342,7 +5342,7 @@ defmodule LightningWeb.ProjectLiveTest do floki_fragment = view |> render_async() |> Floki.parse_fragment!() [branches_input] = Floki.find(floki_fragment, "#select-branches-input") - options = Floki.children(branches_input) + options = Floki.find(branches_input, "li[role=option]") assert Enum.count(options) == 1 options |> hd() |> Floki.raw_html() =~ "Select a branch" @@ -5367,7 +5367,7 @@ defmodule LightningWeb.ProjectLiveTest do # we should now have the branches listed floki_fragment = view |> render_async() |> Floki.parse_fragment!() [branches_input] = Floki.find(floki_fragment, "#select-branches-input") - options = Floki.children(branches_input) + options = Floki.find(branches_input, "li[role=option]") assert Enum.count(options) == 2 [default_option, branch_option] = options Floki.raw_html(default_option) =~ "Select a branch" From 00fd48ec86d0129925fd9072b64438014ffd3d3a Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Fri, 9 Oct 2026 10:01:24 +0100 Subject: [PATCH 08/25] Add tests for the Sync tab form, legacy switch and filter hook Co-Authored-By: Claude Sonnet 5.5 --- assets/test/hooks/SearchableSelect.test.ts | 125 +++++++++++ .../live/project_live/github_sync_ui_test.exs | 206 ++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 assets/test/hooks/SearchableSelect.test.ts create mode 100644 test/lightning_web/live/project_live/github_sync_ui_test.exs diff --git a/assets/test/hooks/SearchableSelect.test.ts b/assets/test/hooks/SearchableSelect.test.ts new file mode 100644 index 0000000000..5217dbaaa7 --- /dev/null +++ b/assets/test/hooks/SearchableSelect.test.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { SearchableSelect } from '#/hooks/SearchableSelect'; + +const OPTIONS = ['Select a repository', 'openfn/lightning', 'openfn/kit']; + +function setup() { + document.body.innerHTML = ` +
+ +
    +
  • + ${OPTIONS.map(o => `
  • ${o}
  • `).join('')} +
+
+ `; + + const el = document.getElementById('repo-searchable') as HTMLElement; + const search = el.querySelector('input')!; + const hook = Object.assign(Object.create(SearchableSelect), { el }); + hook.mounted(); + + const options = () => + Array.from(el.querySelectorAll('li[role=option]')); + const visible = () => + options() + .filter(o => !o.hidden) + .map(o => o.textContent); + const type = (value: string) => { + search.value = value; + search.dispatchEvent(new Event('input')); + }; + + return { el, search, hook, options, visible, type }; +} + +const nextFrame = () => new Promise(resolve => requestAnimationFrame(resolve)); + +describe('SearchableSelect', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('shows every option before anything is typed', () => { + const { visible } = setup(); + + expect(visible()).toEqual(OPTIONS); + }); + + it('hides options that do not match, ignoring case', () => { + const { visible, type } = setup(); + + type('LIGHT'); + + expect(visible()).toEqual(['openfn/lightning']); + }); + + it('shows everything again when the filter is cleared', () => { + const { visible, type } = setup(); + + type('kit'); + type(''); + + expect(visible()).toEqual(OPTIONS); + }); + + it('never hides the search row itself', () => { + const { el, type } = setup(); + + type('nothing matches this'); + + expect(el.querySelector('li[role=presentation]')!.hidden).toBe( + false + ); + }); + + it('reapplies the filter after a LiveView patch', () => { + const { el, hook, visible, type } = setup(); + + type('kit'); + + // a patch re-renders the list with a new option and no hidden flags + const added = document.createElement('li'); + added.setAttribute('role', 'option'); + added.textContent = 'openfn/kitchen'; + el.querySelector('ul')!.appendChild(added); + el.querySelectorAll('li[role=option]').forEach( + o => (o.hidden = false) + ); + + hook.updated(); + + expect(visible()).toEqual(['openfn/kit', 'openfn/kitchen']); + }); + + it('clicks the first visible option on Enter without submitting', () => { + const { search, options, type } = setup(); + const clicked: (string | null)[] = []; + options().forEach(o => + o.addEventListener('click', () => clicked.push(o.textContent)) + ); + + type('kit'); + const event = new KeyboardEvent('keydown', { + key: 'Enter', + cancelable: true, + }); + search.dispatchEvent(event); + + expect(event.defaultPrevented).toBe(true); + expect(clicked).toEqual(['openfn/kit']); + }); + + it('clears the filter and focuses the box when the list is opened', async () => { + const { el, search, visible, type } = setup(); + + type('kit'); + el.querySelector('button')!.click(); + await nextFrame(); + + expect(search.value).toBe(''); + expect(visible()).toEqual(OPTIONS); + expect(document.activeElement).toBe(search); + }); +}); diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs new file mode 100644 index 0000000000..282f961899 --- /dev/null +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -0,0 +1,206 @@ +defmodule LightningWeb.ProjectLive.GithubSyncUiTest do + @moduledoc """ + Tests for the Sync tab UI on project settings: the new connection form (title, + searchable dropdowns, legacy format switch) and the connected-state panel. + """ + + use LightningWeb.ConnCase, async: false + + import Phoenix.LiveViewTest + import Lightning.Factories + import Lightning.GithubHelpers + import Mox + + setup :stub_usage_limiter_ok + setup :verify_on_exit! + + @installation %{ + "id" => "1234", + "account" => %{"type" => "User", "login" => "username"} + } + + @switch "#toggle-legacy-format-switch" + @sync_version_input ~s{input[name="connection[sync_version]"]} + + defp open_new_connection_form(conn) do + project = insert(:project) + {conn, user} = setup_project_user(conn, project, :admin) + set_valid_github_oauth_token!(user) + + expect_get_user_installations(200, %{"installations" => [@installation]}) + expect_create_installation_token(@installation["id"]) + expect_get_installation_repos(200, %{"repositories" => []}) + + {:ok, view, _html} = live(conn, ~p"/projects/#{project.id}/settings#vcs") + render_async(view) + + view + end + + defp stub_failed_verification do + Mox.stub(Lightning.Tesla.Mock, :call, fn _env, _opts -> + {:ok, %Tesla.Env{status: 404, body: %{"something" => "not right"}}} + end) + end + + defp open_connected_panel(conn, role, connection_attrs) do + project = insert(:project) + {conn, _user} = setup_project_user(conn, project, role) + + insert( + :project_repo_connection, + Keyword.merge( + [ + project: project, + repo: "someaccount/somerepo", + branch: "somebranch", + github_installation_id: "1234" + ], + connection_attrs + ) + ) + + stub_failed_verification() + + {:ok, view, _html} = live(conn, ~p"/projects/#{project.id}/settings#vcs") + + {view, render_async(view)} + end + + describe "new connection form" do + test "shows the title and the docs banner", %{conn: conn} do + view = open_new_connection_form(conn) + + assert has_element?(view, "h6", "Configure Two-Way Sync to GitHub") + + assert has_element?( + view, + ~s{#project-repo-connection-form a[href="https://docs.openfn.org/documentation/link-to-GitHub"]} + ) + end + + test "repository and branch dropdowns are searchable, installation is not", + %{conn: conn} do + view = open_new_connection_form(conn) + + assert has_element?( + view, + "#select-repos-input-searchable input[data-select-search]" + ) + + assert has_element?( + view, + "#select-branches-input-searchable input[data-select-search]" + ) + + refute has_element?(view, "#select-installations-input-searchable") + end + + test "defaults to the v2 format with the legacy switch off", %{conn: conn} do + view = open_new_connection_form(conn) + + assert has_element?(view, "#{@switch}[aria-checked=false]") + assert has_element?(view, ~s{#{@sync_version_input}[value=true]}) + end + + test "flipping the switch selects the legacy format and flips it back", + %{conn: conn} do + view = open_new_connection_form(conn) + + view |> element(@switch) |> render_click() + + assert has_element?(view, "#{@switch}[aria-checked=true]") + assert has_element?(view, ~s{#{@sync_version_input}[value=false]}) + + view |> element(@switch) |> render_click() + + assert has_element?(view, "#{@switch}[aria-checked=false]") + assert has_element?(view, ~s{#{@sync_version_input}[value=true]}) + end + + test "the legacy choice survives changing a dropdown", %{conn: conn} do + view = open_new_connection_form(conn) + + view |> element(@switch) |> render_click() + + view + |> form("#project-repo-connection-form") + |> render_change( + connection: %{github_installation_id: @installation["id"]} + ) + + assert has_element?(view, "#{@switch}[aria-checked=true]") + assert has_element?(view, ~s{#{@sync_version_input}[value=false]}) + end + + test "the switch is hidden when importing from GitHub", %{conn: conn} do + view = open_new_connection_form(conn) + + assert has_element?(view, @switch) + + view + |> form("#project-repo-connection-form") + |> render_change(connection: %{sync_direction: "deploy"}) + + refute has_element?(view, @switch) + end + end + + describe "connected panel" do + test "v2 connections show the title and no legacy note or config path", + %{conn: conn} do + {view, html} = open_connected_panel(conn, :admin, sync_version: true) + + assert has_element?(view, "h6", "Connected to GitHub") + refute html =~ "legacy sync format" + refute html =~ "Path to config" + refute html =~ "Sync version:" + end + + test "v1 connections show the legacy note and the config path", + %{conn: conn} do + {view, html} = open_connected_panel(conn, :admin, sync_version: false) + + assert has_element?(view, "h6", "Connected to GitHub") + + assert html =~ + "This connection uses the legacy sync format (config.json). Reconnect to update to v2 sync formats" + + assert html =~ "Path to config" + end + + test "admins get a remove integration button next to the sync button", + %{conn: conn} do + {view, _html} = open_connected_panel(conn, :admin, sync_version: true) + + assert has_element?( + view, + "#remove-integration-button", + "Remove integration" + ) + + assert has_element?(view, "#initiate-sync-button") + assert has_element?(view, "#remove_connection_modal") + end + + test "editors can't remove the integration", %{conn: conn} do + {view, _html} = open_connected_panel(conn, :editor, sync_version: true) + + refute has_element?(view, "#remove-integration-button") + refute has_element?(view, "#remove_connection_modal") + end + + test "the verification banner renders inside the connected card", + %{conn: conn} do + {view, _html} = open_connected_panel(conn, :admin, sync_version: true) + + assert has_element?(view, "#verify-connection-banner") + + # the banner is a descendant of the card that holds the title + assert has_element?( + view, + "div.bg-white:has(h6) #verify-connection-banner" + ) + end + end +end From f74288c80ab5f90c21145cbabc0fa74438eb09c4 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Fri, 9 Oct 2026 11:01:11 +0100 Subject: [PATCH 09/25] Share the GitHub docs banner across Sync tab states Co-Authored-By: Claude Sonnet 5.5 --- .../components/github_components.ex | 30 ++++++++++ .../project_live/github_sync_component.ex | 1 + .../github_sync_component.html.heex | 18 +----- .../live/project_live/settings.html.heex | 4 +- .../live/project_live/github_sync_ui_test.exs | 56 +++++++++++++++++++ 5 files changed, 92 insertions(+), 17 deletions(-) diff --git a/lib/lightning_web/components/github_components.ex b/lib/lightning_web/components/github_components.ex index 3fcbdd0c6a..7460cb4d46 100644 --- a/lib/lightning_web/components/github_components.ex +++ b/lib/lightning_web/components/github_components.ex @@ -30,6 +30,36 @@ defmodule LightningWeb.Components.GithubComponents do """ end + attr :class, :string, default: "mb-4", doc: "spacing classes for the banner" + + @doc """ + Blue info banner pointing to the GitHub sync docs. Shown inside each state of + the Sync tab's GitHub panel. + """ + def sync_docs_banner(assigns) do + ~H""" +
+ <.icon name="hero-information-circle" class="h-5 w-5 shrink-0 text-blue-500" /> +

+ Learn more about synchronizing your project with GitHub at + <.link + target="_blank" + href="https://docs.openfn.org/documentation/link-to-GitHub" + class="font-medium underline" + > + docs.openfn.org + +

+
+ """ + end + defp build_query_params do config = Application.fetch_env!(:lightning, :github_app) client_id = Keyword.fetch!(config, :client_id) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 47508a2fdc..902181be75 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -4,6 +4,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do use LightningWeb, :live_component alias Lightning.VersionControl alias Lightning.VersionControl.ProjectRepoConnection + alias LightningWeb.Components.GithubComponents alias Phoenix.LiveView.AsyncResult alias Phoenix.LiveView.JS diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index e2db2a4255..0420e1b47e 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -12,22 +12,7 @@
Configure Two-Way Sync to GitHub
-
- <.icon - name="hero-information-circle" - class="h-5 w-5 shrink-0 text-blue-500" - /> -

- Learn more about synchronizing your project with GitHub at - <.link - target="_blank" - href="https://docs.openfn.org/documentation/link-to-GitHub" - class="font-medium underline" - > - docs.openfn.org - -

-
+ <%!-- Token is expired/invalid --%> <.error_banner :if={show_invalid_oauth_error?(@installations)} @@ -216,6 +201,7 @@
Connected to GitHub
+ <.verify_connection_banner id="verify-connection-banner" verify_connection={@verify_connection} diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index c357f0df49..088720611e 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -74,7 +74,7 @@ else: "Projects are isolated workspaces that contain workflows, accessible to certain users." } - permissions_message="basic settings, but you can export a copy." + permissions_message="basic settings." can_perform_action={@can_edit_project} /> <.sandbox_settings_banner @@ -807,6 +807,7 @@
Connect your OpenFn account to GitHub
+ Connect your OpenFn account to GitHub. Click the button below to get started. @@ -825,6 +826,7 @@
Connect to GitHub
+ Version Control is not configured for this Lightning instance. Contact the superuser for more information. diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index 282f961899..d1303d8a8e 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -10,6 +10,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do import Lightning.Factories import Lightning.GithubHelpers import Mox + import Lightning.ApplicationHelpers, only: [put_temporary_env: 3] setup :stub_usage_limiter_ok setup :verify_on_exit! @@ -146,6 +147,61 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do end end + describe "docs banner" do + @banner "#github-sync-docs-banner" + + test "shows in the new connection form", %{conn: conn} do + view = open_new_connection_form(conn) + + assert has_element?(view, "#project-repo-connection-form #{@banner}") + end + + test "shows when the user must first connect their GitHub account", + %{conn: conn} do + project = insert(:project) + {conn, _user} = setup_project_user(conn, project, :admin) + + {:ok, view, _html} = live(conn, ~p"/projects/#{project.id}/settings#vcs") + + assert has_element?(view, "#connect-github-link") + assert has_element?(view, @banner) + end + + test "shows when version control is not configured on the instance", + %{conn: conn} do + put_temporary_env(:lightning, :github_app, + cert: nil, + app_id: nil, + app_name: nil, + client_id: nil, + client_secret: nil + ) + + project = insert(:project) + {conn, _user} = setup_project_user(conn, project, :viewer) + + {:ok, view, html} = live(conn, ~p"/projects/#{project.id}/settings#vcs") + + assert html =~ "Version Control is not configured" + assert has_element?(view, @banner) + end + + test "shows on the connected panel", %{conn: conn} do + {view, _html} = open_connected_panel(conn, :admin, sync_version: true) + + assert has_element?(view, "div.bg-white:has(h6) #{@banner}") + end + + test "links to the sync docs", %{conn: conn} do + view = open_new_connection_form(conn) + + assert has_element?( + view, + ~s{#{@banner} a[target="_blank"][href="https://docs.openfn.org/documentation/link-to-GitHub"]} + ) + end + end + describe "connected panel" do test "v2 connections show the title and no legacy note or config path", %{conn: conn} do From fdfd620932887c51e03544360b0e570cf6e46240 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Fri, 9 Oct 2026 11:01:27 +0100 Subject: [PATCH 10/25] Add changelog entries for the Sync tab changes (#5235) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be5660640..f427d16477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ and this project adheres to [#5202](https://github.com/OpenFn/lightning/pull/5202) - Index to improve the performance of the dataclip deletion query. [#5226](https://github.com/OpenFn/lightning/issues/5226) +- The Repository and Branch dropdowns on the GitHub sync form now have a text + filter. [#5235](https://github.com/OpenFn/lightning/pull/5235) ### Changed @@ -84,6 +86,12 @@ and this project adheres to donuts show the success rate and the failure count in their middle. Triage rows show each failure's state and adaptor, and the step bars show each step's adaptor. [#5229](https://github.com/OpenFn/lightning/pull/5229) +- The project settings "Sync to GitHub" tab is now "Sync", and holds the project + export and a new CLI section with commands for pulling, deploying and running + a project locally. New GitHub connections default to the v2 `openfn.yaml` + format, with a switch to use the legacy format, and the connected panel now + carries its own title, verification banner and a "Remove integration" button. + [#5235](https://github.com/OpenFn/lightning/pull/5235) ### Removed From d216e07adbd02e7d1be63dfdffdce2c00d786755 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 12 Oct 2026 16:55:34 +0100 Subject: [PATCH 11/25] Use shared components on the Sync tab The docs banner now uses the shared alert, and the format switch uses the shared toggle. The toggle is on for openfn.yaml and off for legacy config.json, so the custom event that flipped it is gone. The CLI commands get copy buttons using the existing Copy hook. --- .../components/github_components.ex | 15 ++--- .../project_live/github_sync_component.ex | 59 ++----------------- .../github_sync_component.html.heex | 1 - .../live/project_live/settings.html.heex | 41 +++++++------ .../live/project_live/github_sync_ui_test.exs | 32 +++++----- 5 files changed, 51 insertions(+), 97 deletions(-) diff --git a/lib/lightning_web/components/github_components.ex b/lib/lightning_web/components/github_components.ex index 7460cb4d46..554dd8ef2b 100644 --- a/lib/lightning_web/components/github_components.ex +++ b/lib/lightning_web/components/github_components.ex @@ -38,15 +38,8 @@ defmodule LightningWeb.Components.GithubComponents do """ def sync_docs_banner(assigns) do ~H""" -
- <.icon name="hero-information-circle" class="h-5 w-5 shrink-0 text-blue-500" /> -

+ + <:message> Learn more about synchronizing your project with GitHub at <.link target="_blank" @@ -55,8 +48,8 @@ defmodule LightningWeb.Components.GithubComponents do > docs.openfn.org -

-
+ + """ end diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 902181be75..728d3bc30c 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -92,24 +92,6 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do end end - def handle_event("toggle-legacy-format", _params, socket) do - changeset = socket.assigns.changeset - current = Ecto.Changeset.get_field(changeset, :sync_version) - - # Form fields read from the changeset's params before its changes, so the - # flip has to go through the params, like the "validate" event does. - params = - (changeset.params || %{}) - |> Map.put("project_id", socket.assigns.project.id) - |> Map.put("sync_version", to_string(!current)) - - {:noreply, - assign(socket, - changeset: - validate_changes(socket.assigns.project_repo_connection, params) - )} - end - def handle_event("refresh-installations", _params, socket) do changeset = validate_changes(socket.assigns.project_repo_connection, %{}) user = socket.assigns.user @@ -752,45 +734,16 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do attr :form, :map, required: true attr :project_id, :string, required: true - attr :myself, :any, required: true - # Inverted: sync_version is true (v2) unless the legacy switch is on. The - # hidden input carries the current value with the form's phx-change events. defp config_format_toggle(assigns) do - assigns = assign(assigns, :legacy?, !sync_version?(assigns.form)) - ~H""" -
- + <.input + id="sync-version-toggle" + type="toggle" + field={@form[:sync_version]} + label="Use openfn.yaml format (turn off for legacy config.json)" /> - - - Use legacy YAML config format -
""" end diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index 0420e1b47e..bb6a308e27 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -194,7 +194,6 @@ :if={f[:sync_direction].value != :deploy} form={f} project_id={@project.id} - myself={@myself} />
diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index 088720611e..01736be2e3 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -855,23 +855,30 @@

-
-

- Pull this project into a local workspace: -

-
openfn project pull {@project.id}
-
-
-

- Push local changes back to this project: -

-
openfn project deploy
-
-
-

- Run a workflow: -

-
openfn <workflow-name>
+
"} + ]) + }> +

{label}

+
+
{command}
+ +

diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index d1303d8a8e..a3f9944b5e 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -20,8 +20,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do "account" => %{"type" => "User", "login" => "username"} } - @switch "#toggle-legacy-format-switch" - @sync_version_input ~s{input[name="connection[sync_version]"]} + @switch "#sync-version-toggle" defp open_new_connection_form(conn) do project = insert(:project) @@ -97,32 +96,36 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do refute has_element?(view, "#select-installations-input-searchable") end - test "defaults to the v2 format with the legacy switch off", %{conn: conn} do + test "defaults to the openfn.yaml format with the switch on", + %{conn: conn} do view = open_new_connection_form(conn) - assert has_element?(view, "#{@switch}[aria-checked=false]") - assert has_element?(view, ~s{#{@sync_version_input}[value=true]}) + assert has_element?(view, "#{@switch}[checked]") end - test "flipping the switch selects the legacy format and flips it back", + test "turning the switch off selects the legacy format and back on again", %{conn: conn} do view = open_new_connection_form(conn) - view |> element(@switch) |> render_click() + view + |> form("#project-repo-connection-form") + |> render_change(connection: %{sync_version: "false"}) - assert has_element?(view, "#{@switch}[aria-checked=true]") - assert has_element?(view, ~s{#{@sync_version_input}[value=false]}) + refute has_element?(view, "#{@switch}[checked]") - view |> element(@switch) |> render_click() + view + |> form("#project-repo-connection-form") + |> render_change(connection: %{sync_version: "true"}) - assert has_element?(view, "#{@switch}[aria-checked=false]") - assert has_element?(view, ~s{#{@sync_version_input}[value=true]}) + assert has_element?(view, "#{@switch}[checked]") end test "the legacy choice survives changing a dropdown", %{conn: conn} do view = open_new_connection_form(conn) - view |> element(@switch) |> render_click() + view + |> form("#project-repo-connection-form") + |> render_change(connection: %{sync_version: "false"}) view |> form("#project-repo-connection-form") @@ -130,8 +133,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do connection: %{github_installation_id: @installation["id"]} ) - assert has_element?(view, "#{@switch}[aria-checked=true]") - assert has_element?(view, ~s{#{@sync_version_input}[value=false]}) + refute has_element?(view, "#{@switch}[checked]") end test "the switch is hidden when importing from GitHub", %{conn: conn} do From cdb5061e4dfcf139c18933bbed4e4d023d47729d Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 12 Oct 2026 17:03:40 +0100 Subject: [PATCH 12/25] Tone down the GitHub sync confirm box The box was bright amber, and on import it set amber and red backgrounds at once. It now uses pale yellow or pale red with a border, smaller text, and file paths that wrap under their icon. --- .../project_live/github_sync_component.ex | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 728d3bc30c..7664d7f15b 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -821,8 +821,11 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do defp accept_checkbox(assigns) do ~H"""

<.input type="checkbox" @@ -830,7 +833,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do required="true" hidden_input={false} /> - + I understand that <%= if @form[:sync_direction].value == :deploy do %> my current OpenFn project will be destroyed and @@ -838,25 +841,28 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do the following files will be committed to <%= @form[ :repo ].value %>: -
    -
  • - <.icon name="hero-document-plus" class="h-4 w-4" /> - +
      +
    • + <.icon name="hero-document-plus" class="h-4 w-4 shrink-0 text-gray-500" /> + .github/workflows/openfn-pull.yml -> {@default_branch}
    • -
    • - <.icon name="hero-document-plus" class="h-4 w-4" /> - +
    • + <.icon name="hero-document-plus" class="h-4 w-4 shrink-0 text-gray-500" /> + .github/workflows/openfn-{@project.id}-deploy.yml -> {@form[ :branch ].value}
    • <%= if to_string(@form[:config_path].value) == "" do %> -
    • - <.icon name="hero-document-plus" class="h-4 w-4" /> - +
    • + <.icon + name="hero-document-plus" + class="h-4 w-4 shrink-0 text-gray-500" + /> + {config_filename(@form, @project.id)} -> {@form[:branch].value}
    • From 2649d8a39c125a2586c7ee266ae50b25b5b4dbe8 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 12 Oct 2026 17:10:00 +0100 Subject: [PATCH 13/25] Tidy spacing and buttons on the Sync tab The connect GitHub button now uses the shared button, so it matches the others and is really disabled when GitHub sync is limited. Before, it only looked disabled. The GitHub cards use the same spacing as the other cards. The format toggle moves above the submit button, with the same gap as the rest of the form. --- lib/lightning_web/components/github_components.ex | 15 ++++++--------- .../live/project_live/github_sync_component.ex | 2 +- .../project_live/github_sync_component.html.heex | 12 ++++++------ .../live/project_live/settings.html.heex | 14 +++++++------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/lib/lightning_web/components/github_components.ex b/lib/lightning_web/components/github_components.ex index 554dd8ef2b..5f1b599569 100644 --- a/lib/lightning_web/components/github_components.ex +++ b/lib/lightning_web/components/github_components.ex @@ -4,11 +4,6 @@ defmodule LightningWeb.Components.GithubComponents do use LightningWeb, :component attr :id, :string, required: true - - attr :class, :string, - default: - "text-center py-2 px-4 shadow-xs text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 bg-primary-600 hover:bg-primary-700 text-white" - attr :user, Lightning.Accounts.User, required: true attr :github_query_params, :map attr :disabled, :boolean, default: false @@ -18,15 +13,17 @@ defmodule LightningWeb.Components.GithubComponents do assign_new(assigns, :github_query_params, fn -> build_query_params() end) ~H""" - <.link + <.button_link id={@id} + theme="primary" href={"https://github.com/login/oauth/authorize?" <> Plug.Conn.Query.encode(@github_query_params)} target="_blank" - class={[@class, "#{if @disabled, do: "bg-primary-300 cursor-not-allowed"}"]} - {if @user.github_oauth_token, do: ["phx-hook": "Tooltip", "aria-label": "Your token has expired"], else: []} + disabled={@disabled} + phx-hook={@user.github_oauth_token && "Tooltip"} + aria-label={@user.github_oauth_token && "Your token has expired"} > {if @user.github_oauth_token, do: "Reconnect", else: "Connect"} your GitHub Account - + """ end diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 7664d7f15b..8188a81b15 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -737,7 +737,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do defp config_format_toggle(assigns) do ~H""" -
      +
      <.input id="sync-version-toggle" type="toggle" diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index bb6a308e27..3ae5e00afe 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -157,6 +157,11 @@
      <.sync_order_radio form={f} />
      + <.config_format_toggle + :if={f[:sync_direction].value != :deploy} + form={f} + project_id={@project.id} + />
      <.input type="text" @@ -178,7 +183,7 @@ } /> <% end %> -
      +
      <.button id="connect-and-sync-button" type="submit" @@ -190,11 +195,6 @@ Connect Branch & Initiate First Sync
      - <.config_format_toggle - :if={f[:sync_direction].value != :deploy} - form={f} - project_id={@project.id} - />
      diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index 01736be2e3..4b1603f62a 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -802,16 +802,16 @@ />
      Connect your OpenFn account to GitHub
      - - + + Connect your OpenFn account to GitHub. Click the button below to get started. -
      +
      <% end %> <% else %> -
      +
      Connect to GitHub
      - - + + Version Control is not configured for this Lightning instance. Contact the superuser for more information.
      From 95eece7aefb3ce286426f2acbad858be40240e32 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 12 Oct 2026 17:12:25 +0100 Subject: [PATCH 14/25] Add copy feedback to the CLI commands on the Sync tab The copy buttons sit in a tooltip wrapper, so hovering shows "Copy to clipboard" and clicking shows "Copied!". They also get a pointer cursor and a centred icon. --- .../live/project_live/settings.html.heex | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index 4b1603f62a..044f4d8146 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -868,16 +868,22 @@

      {label}

      {command}
      - + +
      From 082193b772d4cb1b7ce9e287c6f933865dff118d Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 12 Oct 2026 17:19:05 +0100 Subject: [PATCH 15/25] Centre the connection check banner on the Sync tab --- lib/lightning_web/live/project_live/github_sync_component.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 8188a81b15..73dbee7403 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -597,8 +597,8 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do
      -
      -
      +
      +
      From 90f807adf8413c1921c7530e6bb3a2bdc1a5871c Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 12 Oct 2026 18:05:23 +0100 Subject: [PATCH 16/25] Tidy wording on the Sync tab --- .../project_live/github_sync_component.ex | 2 +- .../github_sync_component.html.heex | 2 +- .../live/project_live/settings.html.heex | 23 ++++++++----------- .../live/project_live/github_sync_ui_test.exs | 2 +- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 73dbee7403..332ab69e4d 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -761,7 +761,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do Initial Setup Action

      - Choose how to handle setup: you can export this project to GitHub, or import an existing project on GitHub here. + Export this project to GitHub, or replace it with a project that's already on GitHub.

      Direction of Initial Sync diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index 3ae5e00afe..5ef0c6ed94 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -10,7 +10,7 @@ phx-target={@myself} >
      - Configure Two-Way Sync to GitHub + Configure two-way sync to GitHub
      <%!-- Token is expired/invalid --%> diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index 044f4d8146..7dda06102a 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -743,7 +743,7 @@ <:panel hash="vcs" class="space-y-4"> <.section_header title="Sync" - subtitle="Sync your project to version control or other environments from here." + subtitle="Sync your project with GitHub, export it, or work on it with the CLI." permissions_message={"version control configuration#{@can_initiate_github_sync && " but you can initiate a sync." || "."}"} can_perform_action={@can_install_github} /> @@ -809,7 +809,7 @@ - Connect your OpenFn account to GitHub. Click the button below to get started. + You need to do this before you can set up sync.
      -
      Export your Project
      +
      Export your project

      Save your project as a YAML file to be edited locally, backed up, or deployed elsewhere.

      @@ -851,7 +851,12 @@
      Sync with CLI

      - Use the CLI to push and pull this project locally + Use the OpenFn CLI to work on this project on your computer. Learn more in the <.link + target="_blank" + href="https://docs.openfn.org/documentation/sync" + class="font-medium text-indigo-600 hover:text-indigo-900" + > + CLI sync docs.

      @@ -887,16 +892,6 @@
      -

      - Learn more in the - <.link - target="_blank" - href="https://docs.openfn.org/documentation/sync" - class="font-medium text-indigo-600 hover:text-indigo-900" - > - CLI Sync Docs - -

      <:panel hash="data-storage" class="space-y-4"> diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index a3f9944b5e..54af23ead5 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -71,7 +71,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do test "shows the title and the docs banner", %{conn: conn} do view = open_new_connection_form(conn) - assert has_element?(view, "h6", "Configure Two-Way Sync to GitHub") + assert has_element?(view, "h6", "Configure two-way sync to GitHub") assert has_element?( view, From ab021a05dcc341255de948c2fe5c41446282c4a8 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 12 Oct 2026 18:07:33 +0100 Subject: [PATCH 17/25] Show the sync confirm box only after a branch is picked --- .../github_sync_component.html.heex | 2 +- .../live/project_live/github_sync_ui_test.exs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index 5ef0c6ed94..a1e4d6bd4b 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -171,7 +171,7 @@ class="placeholder:italic placeholder:text-slate-400" />
      - <%= if f[:branch].value do %> + <%= if f[:branch].value not in [nil, ""] do %> <.accept_checkbox project={@project} form={f} diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index 54af23ead5..8ab6cb7735 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -147,6 +147,25 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do refute has_element?(view, @switch) end + + test "the confirm box waits until a branch is picked", %{conn: conn} do + view = open_new_connection_form(conn) + confirm = ~s{input[name="connection[accept]"]} + + view + |> form("#project-repo-connection-form") + |> render_change( + connection: %{github_installation_id: @installation["id"]} + ) + + refute has_element?(view, confirm) + + view + |> form("#project-repo-connection-form") + |> render_change(connection: %{branch: "main"}) + + assert has_element?(view, confirm) + end end describe "docs banner" do From 82a6a3b92c1f61a23fe6820c060881a1e41f7f45 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 12 Oct 2026 18:18:29 +0100 Subject: [PATCH 18/25] Bring back the legacy format switch on the Sync tab The shared toggle could only mean "use openfn.yaml", which read the wrong way round. This restores the hand-built switch, where on means the legacy format. --- .../project_live/github_sync_component.ex | 59 +++++++++++++++++-- .../github_sync_component.html.heex | 1 + .../live/project_live/github_sync_ui_test.exs | 32 +++++----- 3 files changed, 69 insertions(+), 23 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 332ab69e4d..466965cd7c 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -92,6 +92,24 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do end end + def handle_event("toggle-legacy-format", _params, socket) do + changeset = socket.assigns.changeset + current = Ecto.Changeset.get_field(changeset, :sync_version) + + # Form fields read from the changeset's params before its changes, so the + # flip has to go through the params, like the "validate" event does. + params = + (changeset.params || %{}) + |> Map.put("project_id", socket.assigns.project.id) + |> Map.put("sync_version", to_string(!current)) + + {:noreply, + assign(socket, + changeset: + validate_changes(socket.assigns.project_repo_connection, params) + )} + end + def handle_event("refresh-installations", _params, socket) do changeset = validate_changes(socket.assigns.project_repo_connection, %{}) user = socket.assigns.user @@ -734,16 +752,45 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do attr :form, :map, required: true attr :project_id, :string, required: true + attr :myself, :any, required: true + # Inverted: sync_version is true (v2) unless the legacy switch is on. The + # hidden input carries the current value with the form's phx-change events. defp config_format_toggle(assigns) do + assigns = assign(assigns, :legacy?, !sync_version?(assigns.form)) + ~H""" -
      - <.input - id="sync-version-toggle" - type="toggle" - field={@form[:sync_version]} - label="Use openfn.yaml format (turn off for legacy config.json)" +
      + + + + Use legacy YAML config format +
      """ end diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index a1e4d6bd4b..efb13bc60f 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -161,6 +161,7 @@ :if={f[:sync_direction].value != :deploy} form={f} project_id={@project.id} + myself={@myself} />
      <.input diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index 8ab6cb7735..b7c691281f 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -20,7 +20,8 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do "account" => %{"type" => "User", "login" => "username"} } - @switch "#sync-version-toggle" + @switch "#toggle-legacy-format-switch" + @sync_version_input ~s{input[name="connection[sync_version]"]} defp open_new_connection_form(conn) do project = insert(:project) @@ -96,36 +97,32 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do refute has_element?(view, "#select-installations-input-searchable") end - test "defaults to the openfn.yaml format with the switch on", - %{conn: conn} do + test "defaults to the v2 format with the legacy switch off", %{conn: conn} do view = open_new_connection_form(conn) - assert has_element?(view, "#{@switch}[checked]") + assert has_element?(view, "#{@switch}[aria-checked=false]") + assert has_element?(view, ~s{#{@sync_version_input}[value=true]}) end - test "turning the switch off selects the legacy format and back on again", + test "flipping the switch selects the legacy format and flips it back", %{conn: conn} do view = open_new_connection_form(conn) - view - |> form("#project-repo-connection-form") - |> render_change(connection: %{sync_version: "false"}) + view |> element(@switch) |> render_click() - refute has_element?(view, "#{@switch}[checked]") + assert has_element?(view, "#{@switch}[aria-checked=true]") + assert has_element?(view, ~s{#{@sync_version_input}[value=false]}) - view - |> form("#project-repo-connection-form") - |> render_change(connection: %{sync_version: "true"}) + view |> element(@switch) |> render_click() - assert has_element?(view, "#{@switch}[checked]") + assert has_element?(view, "#{@switch}[aria-checked=false]") + assert has_element?(view, ~s{#{@sync_version_input}[value=true]}) end test "the legacy choice survives changing a dropdown", %{conn: conn} do view = open_new_connection_form(conn) - view - |> form("#project-repo-connection-form") - |> render_change(connection: %{sync_version: "false"}) + view |> element(@switch) |> render_click() view |> form("#project-repo-connection-form") @@ -133,7 +130,8 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do connection: %{github_installation_id: @installation["id"]} ) - refute has_element?(view, "#{@switch}[checked]") + assert has_element?(view, "#{@switch}[aria-checked=true]") + assert has_element?(view, ~s{#{@sync_version_input}[value=false]}) end test "the switch is hidden when importing from GitHub", %{conn: conn} do From 1e8c71c2772f51a7e7760e739dd1bd85b2db1c8e Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Tue, 13 Oct 2026 11:02:28 +0100 Subject: [PATCH 19/25] hide config.json path in v2 and update legacy format label --- .../project_repo_connection.ex | 4 +- .../project_live/github_sync_component.ex | 2 +- .../github_sync_component.html.heex | 15 +++-- .../project_repo_connection_test.exs | 52 ++++++++++++++++ .../live/project_live/github_sync_ui_test.exs | 62 ++++++++++++++++++- 5 files changed, 123 insertions(+), 12 deletions(-) diff --git a/lib/lightning/version_control/project_repo_connection.ex b/lib/lightning/version_control/project_repo_connection.ex index 65f65b097e..6949156f2d 100644 --- a/lib/lightning/version_control/project_repo_connection.ex +++ b/lib/lightning/version_control/project_repo_connection.ex @@ -204,7 +204,9 @@ defmodule Lightning.VersionControl.ProjectRepoConnection do validate_change(changeset, :sync_direction, fn _field, value -> path = get_field(changeset, :config_path) - if value == :deploy and is_nil(path) do + # v2 syncs always use openfn.yaml, so only legacy ones need a path + if value == :deploy and is_nil(path) and + not get_field(changeset, :sync_version) do [config_path: "you must specify a path to an existing config file"] else [] diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 466965cd7c..e8b7612e58 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -789,7 +789,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do - Use legacy YAML config format + Use legacy formats (config.json)
      """ diff --git a/lib/lightning_web/live/project_live/github_sync_component.html.heex b/lib/lightning_web/live/project_live/github_sync_component.html.heex index efb13bc60f..798bf72d05 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.html.heex +++ b/lib/lightning_web/live/project_live/github_sync_component.html.heex @@ -157,17 +157,16 @@
      <.sync_order_radio form={f} />
      - <.config_format_toggle - :if={f[:sync_direction].value != :deploy} - form={f} - project_id={@project.id} - myself={@myself} - /> -
      + <.config_format_toggle form={f} project_id={@project.id} myself={@myself} /> +
      <.input type="text" field={f[:config_path]} - label="Path to config (required)" + label={ + if f[:sync_direction].value == :deploy, + do: "Path to config (required)", + else: "Path to config (optional)" + } placeholder={"./openfn-#{@project.id}-config.json"} class="placeholder:italic placeholder:text-slate-400" /> diff --git a/test/lightning/version_control/project_repo_connection_test.exs b/test/lightning/version_control/project_repo_connection_test.exs index 92cd7e57d4..77b8ac4ced 100644 --- a/test/lightning/version_control/project_repo_connection_test.exs +++ b/test/lightning/version_control/project_repo_connection_test.exs @@ -233,6 +233,58 @@ defmodule Lightning.VersionControl.ProjectRepoConnectionTest do end end + describe "config path when importing from GitHub (configure_changeset/2)" do + setup do + %{ + attrs: %{ + project_id: insert(:project).id, + repo: "openfn/example", + branch: "main", + github_installation_id: "1234", + sync_direction: "deploy", + accept: true + } + } + end + + test "a legacy import needs a path to config", %{attrs: attrs} do + changeset = + ProjectRepoConnection.configure_changeset( + %ProjectRepoConnection{}, + Map.put(attrs, :sync_version, false) + ) + + refute changeset.valid? + + assert {"you must specify a path to an existing config file", _} = + changeset.errors[:config_path] + end + + test "a legacy import is valid with a path", %{attrs: attrs} do + changeset = + ProjectRepoConnection.configure_changeset( + %ProjectRepoConnection{}, + Map.merge(attrs, %{sync_version: false, config_path: "./config.json"}) + ) + + assert changeset.valid? + end + + test "a v2 import needs no path and uses openfn.yaml", %{attrs: attrs} do + changeset = + ProjectRepoConnection.configure_changeset( + %ProjectRepoConnection{}, + Map.put(attrs, :sync_version, true) + ) + + assert changeset.valid? + + assert ProjectRepoConnection.config_path( + Ecto.Changeset.apply_changes(changeset) + ) == "openfn.yaml" + end + end + describe "tree_branch_conflict?/4" do test "returns false when no other connection in the tree uses the (repo, branch)" do root = insert(:project) diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index b7c691281f..607f3635a2 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -22,6 +22,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do @switch "#toggle-legacy-format-switch" @sync_version_input ~s{input[name="connection[sync_version]"]} + @config_path_input ~s{input[name="connection[config_path]"]} defp open_new_connection_form(conn) do project = insert(:project) @@ -134,16 +135,73 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do assert has_element?(view, ~s{#{@sync_version_input}[value=false]}) end - test "the switch is hidden when importing from GitHub", %{conn: conn} do + test "the switch stays visible when importing from GitHub", %{conn: conn} do view = open_new_connection_form(conn) + view + |> form("#project-repo-connection-form") + |> render_change(connection: %{sync_direction: "deploy"}) + assert has_element?(view, @switch) + end + + test "a v2 import has no path to config input", %{conn: conn} do + view = open_new_connection_form(conn) view |> form("#project-repo-connection-form") |> render_change(connection: %{sync_direction: "deploy"}) - refute has_element?(view, @switch) + assert has_element?(view, "#{@switch}[aria-checked=false]") + refute has_element?(view, @config_path_input) + end + + test "a legacy import asks for the path to config", %{conn: conn} do + view = open_new_connection_form(conn) + + view + |> form("#project-repo-connection-form") + |> render_change(connection: %{sync_direction: "deploy"}) + + view |> element(@switch) |> render_click() + + assert has_element?(view, @config_path_input) + + view |> element(@switch) |> render_click() + + refute has_element?(view, @config_path_input) + end + + test "an export-first v2 sync has no path to config", %{conn: conn} do + view = open_new_connection_form(conn) + + refute has_element?(view, @config_path_input) + end + + test "an export-first legacy sync shows an optional path to config", + %{conn: conn} do + view = open_new_connection_form(conn) + + view |> element(@switch) |> render_click() + + assert has_element?(view, @config_path_input) + assert render(view) =~ "Path to config (optional)" + + view |> element(@switch) |> render_click() + + refute has_element?(view, @config_path_input) + end + + test "a legacy import marks the path to config as required", %{conn: conn} do + view = open_new_connection_form(conn) + + view + |> form("#project-repo-connection-form") + |> render_change(connection: %{sync_direction: "deploy"}) + + view |> element(@switch) |> render_click() + + assert render(view) =~ "Path to config (required)" end test "the confirm box waits until a branch is picked", %{conn: conn} do From c2db6fcf5893f880613cd2d3677349af8a256073 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Tue, 13 Oct 2026 11:09:03 +0100 Subject: [PATCH 20/25] tooltip for legacy formats --- .../live/project_live/github_sync_component.ex | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index e8b7612e58..8267ab14d2 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -4,6 +4,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do use LightningWeb, :live_component alias Lightning.VersionControl alias Lightning.VersionControl.ProjectRepoConnection + alias LightningWeb.Components.Common alias LightningWeb.Components.GithubComponents alias Phoenix.LiveView.AsyncResult alias Phoenix.LiveView.JS @@ -760,7 +761,13 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do assigns = assign(assigns, :legacy?, !sync_version?(assigns.form)) ~H""" -
      + Use legacy formats (config.json) -
      + """ end From 8153983ed30839f7b4c5d7dd9dacebc81387c8ba Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Tue, 13 Oct 2026 11:25:08 +0100 Subject: [PATCH 21/25] better UI if permissions are disabled --- .../live/project_live/settings.html.heex | 22 ++++- .../live/project_live/github_sync_ui_test.exs | 81 +++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index 7dda06102a..ed3ce0f234 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -744,8 +744,6 @@ <.section_header title="Sync" subtitle="Sync your project with GitHub, export it, or work on it with the CLI." - permissions_message={"version control configuration#{@can_initiate_github_sync && " but you can initiate a sync." || "."}"} - can_perform_action={@can_install_github} /> <.sandbox_settings_banner :if={@sandbox?} @@ -779,6 +777,26 @@ } /> <% else %> +
      +
      + Configure two-way sync to GitHub +
      + + <:message> + Role based permissions: You cannot modify this project's version control configuration{if @can_initiate_github_sync, + do: " but you can initiate a sync.", + else: "."} + + +
      <.live_component :if={user_has_valid_oauth_token(@current_user)} diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index 607f3635a2..05422de3dd 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -279,6 +279,87 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do end end + describe "permissions banner" do + @permissions_banner "#vcs-permissions-banner" + + defp open_sync_tab(conn, role) do + project = insert(:project) + {conn, _user} = setup_project_user(conn, project, role) + + {:ok, view, _html} = live(conn, ~p"/projects/#{project.id}/settings#vcs") + view + end + + test "viewers are told they can't change the connection", %{conn: conn} do + view = open_sync_tab(conn, :viewer) + + assert has_element?( + view, + @permissions_banner, + "You cannot modify this project's version control configuration." + ) + + refute render(view) =~ "but you can initiate a sync" + end + + test "editors are told they can still initiate a sync", %{conn: conn} do + view = open_sync_tab(conn, :editor) + + assert has_element?( + view, + @permissions_banner, + "version control configuration but you can initiate a sync." + ) + end + + test "admins see no permissions banner", %{conn: conn} do + view = open_sync_tab(conn, :admin) + + refute has_element?(view, @permissions_banner) + end + + test "the message sits in a plain box where the form would be", + %{conn: conn} do + view = open_sync_tab(conn, :viewer) + + assert has_element?( + view, + "#vcs-permissions-box h6", + "Configure two-way sync to GitHub" + ) + + assert has_element?( + view, + "#vcs-permissions-box #vcs-permissions-banner" + ) + + # the real form is not rendered, so nothing calls GitHub + refute has_element?(view, "#project-repo-connection-form") + refute has_element?(view, "#connect-github-link") + end + + test "admins get the real form and no permissions box", %{conn: conn} do + view = open_new_connection_form(conn) + + refute has_element?(view, "#vcs-permissions-box") + assert has_element?(view, "#project-repo-connection-form") + end + + test "a connected project shows its connection, not the box", + %{conn: conn} do + {view, _html} = open_connected_panel(conn, :editor, sync_version: true) + + assert has_element?(view, "h6", "Connected to GitHub") + refute has_element?(view, "#vcs-permissions-box") + end + + test "the message no longer sits in the section header", %{conn: conn} do + view = open_sync_tab(conn, :viewer) + + refute has_element?(view, "#vcs-panel small.text-red-700") + end + end + describe "connected panel" do test "v2 connections show the title and no legacy note or config path", %{conn: conn} do From d63f065f0c5670400a22df041ccdd69075033104 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Tue, 13 Oct 2026 11:35:12 +0100 Subject: [PATCH 22/25] link to more docs --- .../live/project_live/settings.html.heex | 10 ++++++++++ .../live/project_live/github_sync_ui_test.exs | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index ed3ce0f234..cbe5938184 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -910,6 +910,16 @@
      +

      + <.link + id="cli-more-examples-link" + target="_blank" + href="https://docs.openfn.org/documentation/cli-usage" + class="font-medium text-indigo-600 hover:text-indigo-900" + > + See more CLI examples + +

      <:panel hash="data-storage" class="space-y-4"> diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index 05422de3dd..656aafc2bd 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -279,6 +279,18 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do end end + describe "CLI card" do + test "links to more CLI examples", %{conn: conn} do + view = open_sync_tab(conn, :viewer) + + assert has_element?( + view, + ~s{#cli-more-examples-link[target="_blank"][href="https://docs.openfn.org/documentation/cli-usage"]}, + "See more CLI examples" + ) + end + end + describe "permissions banner" do @permissions_banner "#vcs-permissions-banner" From 5e8ea0a3f2ebd118e2dc39be595817401086e7c4 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 14 Oct 2026 10:32:56 +0100 Subject: [PATCH 23/25] wording tweaks --- lib/lightning_web/live/project_live/settings.html.heex | 4 ++-- test/lightning_web/live/project_live/github_sync_ui_test.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index cbe5938184..dcce5a3f23 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -869,7 +869,7 @@
      Sync with CLI

      - Use the OpenFn CLI to work on this project on your computer. Learn more in the <.link + Use the OpenFn CLI to download, deploy and execute workflows in this project. Learn more in the <.link target="_blank" href="https://docs.openfn.org/documentation/sync" class="font-medium text-indigo-600 hover:text-indigo-900" @@ -917,7 +917,7 @@ href="https://docs.openfn.org/documentation/cli-usage" class="font-medium text-indigo-600 hover:text-indigo-900" > - See more CLI examples + See more CLI commands

      diff --git a/test/lightning_web/live/project_live/github_sync_ui_test.exs b/test/lightning_web/live/project_live/github_sync_ui_test.exs index 656aafc2bd..42e14f6cc9 100644 --- a/test/lightning_web/live/project_live/github_sync_ui_test.exs +++ b/test/lightning_web/live/project_live/github_sync_ui_test.exs @@ -286,7 +286,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncUiTest do assert has_element?( view, ~s{#cli-more-examples-link[target="_blank"][href="https://docs.openfn.org/documentation/cli-usage"]}, - "See more CLI examples" + "See more CLI commands" ) end end From bf9a41bbc5f01071cc455dd3b23ceaf8e793e407 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 14 Oct 2026 10:39:27 +0100 Subject: [PATCH 24/25] more wording tweaks --- .../live/project_live/github_sync_component.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/lightning_web/live/project_live/github_sync_component.ex b/lib/lightning_web/live/project_live/github_sync_component.ex index 8267ab14d2..41905ce532 100644 --- a/lib/lightning_web/live/project_live/github_sync_component.ex +++ b/lib/lightning_web/live/project_live/github_sync_component.ex @@ -812,10 +812,10 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do ~H"""
      <.label> - Initial Setup Action + Initial Setup

      - Export this project to GitHub, or replace it with a project that's already on GitHub. + Choose the direction of the first sync: export this project to GitHub, or import an existing project from GitHub.

      Direction of Initial Sync @@ -837,7 +837,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do

      - This option will commit a copy of your current OpenFn project to GitHub. It does not affect any workflows or configuration here. + Commit a copy of your current OpenFn project to GitHub. This does not affect any workflows or configuration here.

      @@ -858,7 +858,7 @@ defmodule LightningWeb.ProjectLive.GithubSyncComponent do Import from GitHub (overwrite this project)

      - This option will replace this project with the workflows, collections and channels that are already committed to GitHub. + Replace this OpenFn project with the workflows, collections and channels that are already committed to GitHub.

      From 11b3d8e1515c1694975caff7c195aa7674ba11b8 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Wed, 14 Oct 2026 10:54:11 +0100 Subject: [PATCH 25/25] add PAT note --- .../live/project_live/settings.html.heex | 12 ++++++++++++ .../live/project_live/github_sync_ui_test.exs | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/lib/lightning_web/live/project_live/settings.html.heex b/lib/lightning_web/live/project_live/settings.html.heex index dcce5a3f23..245ad064e0 100644 --- a/lib/lightning_web/live/project_live/settings.html.heex +++ b/lib/lightning_web/live/project_live/settings.html.heex @@ -877,6 +877,18 @@ CLI sync docs.

      + + <:message> + You'll need to configure a Personal Access Token and add it to your environment for these commands to work: see + <.link + target="_blank" + href="https://docs.openfn.org/documentation/sync#authorization" + class="font-medium underline" + > + Authorization docs + + +