From 4b79412b52d563d4401219bc0575bcf469b7e159 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Fri, 19 Dec 2025 16:26:10 +0100 Subject: [PATCH 1/7] wip --- .../components/common/RunBadge.tsx | 2 +- .../components/left-panel/TemplateCard.tsx | 23 +++++++--- .../components/left-panel/TemplatePanel.tsx | 29 ++++++------- .../constants/baseTemplates.ts | 13 +++--- assets/js/manual-run-panel/Badge.tsx | 43 ------------------- .../manual-run-panel/views/ExistingView.tsx | 2 +- .../views/SelectedClipView.tsx | 2 +- .../workflow_live/dashboard_components.ex | 3 +- 8 files changed, 41 insertions(+), 76 deletions(-) delete mode 100644 assets/js/manual-run-panel/Badge.tsx diff --git a/assets/js/collaborative-editor/components/common/RunBadge.tsx b/assets/js/collaborative-editor/components/common/RunBadge.tsx index 4ae10ead29..bbd6a7c7a7 100644 --- a/assets/js/collaborative-editor/components/common/RunBadge.tsx +++ b/assets/js/collaborative-editor/components/common/RunBadge.tsx @@ -9,7 +9,7 @@ * - IDE FullScreenIDE (run selection indicator) */ -import Badge from '#/manual-run-panel/Badge'; +import Badge from './Badge'; interface RunBadgeProps { runId: string; diff --git a/assets/js/collaborative-editor/components/left-panel/TemplateCard.tsx b/assets/js/collaborative-editor/components/left-panel/TemplateCard.tsx index 1c9b25b5dc..068423851b 100644 --- a/assets/js/collaborative-editor/components/left-panel/TemplateCard.tsx +++ b/assets/js/collaborative-editor/components/left-panel/TemplateCard.tsx @@ -1,6 +1,7 @@ import { cn } from '#/utils/cn'; import type { Template } from '../../types/template'; +import Badge from '../common/Badge'; interface TemplateCardProps { template: Template; @@ -57,12 +58,22 @@ export function TemplateCard({ -

- {template.name} -

-

- {template.description || 'No description provided'} -

+
+

+ {template.name} +

+ {'isBase' in template && template.isBase && base} +
+ {'isBase' in template && template.isBase ? null : ( +

+ {template.description || 'No description provided'} +

+ )} ); } diff --git a/assets/js/collaborative-editor/components/left-panel/TemplatePanel.tsx b/assets/js/collaborative-editor/components/left-panel/TemplatePanel.tsx index 6e4861a231..c34251eb2f 100644 --- a/assets/js/collaborative-editor/components/left-panel/TemplatePanel.tsx +++ b/assets/js/collaborative-editor/components/left-panel/TemplatePanel.tsx @@ -72,14 +72,12 @@ export function TemplatePanel({ onImportClick, onImport }: TemplatePanelProps) { }, [channel, uiStore]); const allTemplates: Template[] = useMemo(() => { - const combined = [...BASE_TEMPLATES, ...templates]; - if (!searchQuery.trim()) { - return combined; + return [...BASE_TEMPLATES, ...templates]; } const query = searchQuery.toLowerCase(); - return combined.filter(template => { + const filteredUserTemplates = templates.filter(template => { const matchName = template.name.toLowerCase().includes(query); const matchDesc = template.description?.toLowerCase().includes(query); const matchTags = template.tags.some(tag => @@ -87,6 +85,9 @@ export function TemplatePanel({ onImportClick, onImport }: TemplatePanelProps) { ); return matchName || matchDesc || matchTags; }); + + // Always show base templates, regardless of search query + return [...BASE_TEMPLATES, ...filteredUserTemplates]; }, [templates, searchQuery]); const handleSelectTemplate = useCallback( @@ -117,8 +118,8 @@ export function TemplatePanel({ onImportClick, onImport }: TemplatePanelProps) { }; const handleBuildWithAI = useCallback(() => { - // Only trigger if there are no matching templates and there's a search query - if (allTemplates.length === 0 && searchQuery) { + // Only trigger if no user templates matched and there's a search query + if (allTemplates.length === BASE_TEMPLATES.length && searchQuery) { // Clear any existing AI session and disconnect aiStore.disconnect(); aiStore.clearSession(); @@ -141,7 +142,7 @@ export function TemplatePanel({ onImportClick, onImport }: TemplatePanelProps) {

- Browse templates + Create a new workflow

@@ -214,21 +215,19 @@ export function TemplatePanel({ onImportClick, onImport }: TemplatePanelProps) { /> ))} - {allTemplates.length === 0 && searchQuery && ( + {allTemplates.length === BASE_TEMPLATES.length && searchQuery && (
- +
-

- No matching templates -

- We couldn't find any templates for " + We couldn't find any matches for " {searchQuery} - " + ". Start from scratch with one of these base templates or + click below to generate this workflow with AI.

-
- ); -}; - -export default Badge; diff --git a/assets/js/manual-run-panel/views/ExistingView.tsx b/assets/js/manual-run-panel/views/ExistingView.tsx index c396bd4765..663e2c9c20 100644 --- a/assets/js/manual-run-panel/views/ExistingView.tsx +++ b/assets/js/manual-run-panel/views/ExistingView.tsx @@ -11,7 +11,7 @@ import { cn } from '#/utils/cn'; import formatDate from '#/utils/formatDate'; import truncateUid from '#/utils/truncateUID'; -import Pill from '../Badge'; +import Pill from '#/collaborative-editor/components/common/Badge'; import DataclipTypePill from '../DataclipTypePill'; import { DataclipTypeNames, diff --git a/assets/js/manual-run-panel/views/SelectedClipView.tsx b/assets/js/manual-run-panel/views/SelectedClipView.tsx index 8fb1947ed3..5dbdd68fe2 100644 --- a/assets/js/manual-run-panel/views/SelectedClipView.tsx +++ b/assets/js/manual-run-panel/views/SelectedClipView.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { DataclipViewer } from '../../react/components/DataclipViewer'; import formatDate from '../../utils/formatDate'; -import Pill from '../Badge'; +import Pill from '#/collaborative-editor/components/common/Badge'; import DataclipTypePill from '../DataclipTypePill'; import type { Dataclip } from '../types'; diff --git a/lib/lightning_web/live/workflow_live/dashboard_components.ex b/lib/lightning_web/live/workflow_live/dashboard_components.ex index 905a069017..814d7da2de 100644 --- a/lib/lightning_web/live/workflow_live/dashboard_components.ex +++ b/lib/lightning_web/live/workflow_live/dashboard_components.ex @@ -352,7 +352,8 @@ defmodule LightningWeb.WorkflowLive.DashboardComponents do disabled={@disabled} tooltip={@tooltip} phx-click={ - if !@disabled, do: JS.navigate(~p"/projects/#{@project_id}/w/new") + if !@disabled, + do: JS.navigate(~p"/projects/#{@project_id}/w/new?method=template") } class="col-span-1 w-full" role="button" From ba8c672a4ae31827717f035bd04bf71fac739aef Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Fri, 19 Dec 2025 16:42:58 +0100 Subject: [PATCH 2/7] close #4266 --- CHANGELOG.md | 2 + .../components/common/Badge.tsx | 45 ++++++++++ .../constants/baseTemplates.ts | 20 ++--- .../left-panel/TemplateCard.test.tsx | 88 ++++++++++++++----- 4 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 assets/js/collaborative-editor/components/common/Badge.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d42773ea..723c8489bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to ### Fixed +- Restore "Create workflow" flow + [#4266](https://github.com/OpenFn/lightning/issues/4266) - Allow users to add collaborators with long email domains [#4185](https://github.com/OpenFn/lightning/issues/4185) diff --git a/assets/js/collaborative-editor/components/common/Badge.tsx b/assets/js/collaborative-editor/components/common/Badge.tsx new file mode 100644 index 0000000000..5cdbad8376 --- /dev/null +++ b/assets/js/collaborative-editor/components/common/Badge.tsx @@ -0,0 +1,45 @@ +import { cn } from '#/utils/cn'; + +interface BadgeProps { + onClose?: () => void; + className?: string; + variant?: 'default' | 'warning'; +} + +const Badge: React.FC> = ({ + children, + onClose, + className, + variant = 'default', +}) => { + return ( +
+ {children} + {onClose && ( + + )} +
+ ); +}; + +export default Badge; diff --git a/assets/js/collaborative-editor/constants/baseTemplates.ts b/assets/js/collaborative-editor/constants/baseTemplates.ts index ebfcf4ef55..f03917a384 100644 --- a/assets/js/collaborative-editor/constants/baseTemplates.ts +++ b/assets/js/collaborative-editor/constants/baseTemplates.ts @@ -9,11 +9,11 @@ export const BASE_TEMPLATES: BaseTemplate[] = [ isBase: true, code: `name: "Event-based workflow" jobs: - My-job: - name: Validate & transform data + Transform-data: + name: Transform data adaptor: "@openfn/language-common@latest" body: | - // Start writing your job code here + // Validate and transform the data you've received... fn(state => { console.log("Do some data transformation here"); return state; @@ -23,9 +23,9 @@ triggers: type: webhook enabled: true edges: - webhook->My-job: + webhook->Transform-data: source_trigger: webhook - target_job: My-job + target_job: Transform-data condition_type: always enabled: true`, }, @@ -37,21 +37,21 @@ edges: isBase: true, code: `name: "Scheduled workflow" jobs: - My-job: + Get-data: name: Get data adaptor: "@openfn/language-http@latest" body: | - // Start writing your job code here - get('www.example.com'); + // Get some data from an API... + get('https://www.example.com'); triggers: cron: type: cron cron_expression: "0 0 * * *" enabled: true edges: - cron->My-job: + cron->Get-data: source_trigger: cron - target_job: My-job + target_job: Get-data condition_type: always enabled: true`, }, diff --git a/assets/test/collaborative-editor/components/left-panel/TemplateCard.test.tsx b/assets/test/collaborative-editor/components/left-panel/TemplateCard.test.tsx index eda687b8b6..b79405cb3c 100644 --- a/assets/test/collaborative-editor/components/left-panel/TemplateCard.test.tsx +++ b/assets/test/collaborative-editor/components/left-panel/TemplateCard.test.tsx @@ -14,7 +14,7 @@ import { TemplateCard } from '../../../../js/collaborative-editor/components/lef import type { Template } from '../../../../js/collaborative-editor/types/template'; describe('TemplateCard', () => { - const mockTemplate: Template = { + const mockBaseTemplate: Template = { id: 'test-template-1', name: 'Test Template', description: 'This is a test template description', @@ -23,6 +23,16 @@ describe('TemplateCard', () => { isBase: true, }; + const mockUserTemplate: Template = { + id: 'user-template-1', + name: 'User Template', + description: 'This is a user template description', + code: 'workflow code here', + tags: ['user', 'custom'], + positions: null, + workflow_id: null, + }; + let mockOnClick: ReturnType; beforeEach(() => { @@ -32,7 +42,7 @@ describe('TemplateCard', () => { it('renders template name', () => { render( @@ -41,23 +51,61 @@ describe('TemplateCard', () => { expect(screen.getByText('Test Template')).toBeInTheDocument(); }); - it('renders template description', () => { + it('renders template description for user templates', () => { render( ); expect( - screen.getByText('This is a test template description') + screen.getByText('This is a user template description') ).toBeInTheDocument(); }); + it('hides description for base templates', () => { + render( + + ); + + expect( + screen.queryByText('This is a test template description') + ).not.toBeInTheDocument(); + }); + + it('shows base badge for base templates', () => { + render( + + ); + + expect(screen.getByText('base')).toBeInTheDocument(); + }); + + it('does not show base badge for user templates', () => { + render( + + ); + + expect(screen.queryByText('base')).not.toBeInTheDocument(); + }); + it('renders "No description provided" when description is missing', () => { const templateWithoutDesc: Template = { - ...mockTemplate, + ...mockUserTemplate, description: null, }; @@ -75,7 +123,7 @@ describe('TemplateCard', () => { it('shows selected state visually', () => { const { container } = render( @@ -88,7 +136,7 @@ describe('TemplateCard', () => { it('shows unselected state visually', () => { const { container } = render( @@ -101,7 +149,7 @@ describe('TemplateCard', () => { it('calls onClick when card is clicked', () => { render( @@ -110,13 +158,13 @@ describe('TemplateCard', () => { const card = screen.getByRole('button', { name: /test template/i }); fireEvent.click(card); - expect(mockOnClick).toHaveBeenCalledWith(mockTemplate); + expect(mockOnClick).toHaveBeenCalledWith(mockBaseTemplate); }); it('calls onClick when Enter key is pressed', () => { render( @@ -125,13 +173,13 @@ describe('TemplateCard', () => { const card = screen.getByRole('button', { name: /test template/i }); fireEvent.keyDown(card, { key: 'Enter' }); - expect(mockOnClick).toHaveBeenCalledWith(mockTemplate); + expect(mockOnClick).toHaveBeenCalledWith(mockBaseTemplate); }); it('calls onClick when Space key is pressed', () => { render( @@ -140,13 +188,13 @@ describe('TemplateCard', () => { const card = screen.getByRole('button', { name: /test template/i }); fireEvent.keyDown(card, { key: ' ' }); - expect(mockOnClick).toHaveBeenCalledWith(mockTemplate); + expect(mockOnClick).toHaveBeenCalledWith(mockBaseTemplate); }); it('does not call onClick for other keys', () => { render( @@ -161,7 +209,7 @@ describe('TemplateCard', () => { it('has correct accessibility attributes when selected', () => { render( @@ -175,7 +223,7 @@ describe('TemplateCard', () => { it('has correct accessibility attributes when not selected', () => { render( @@ -189,7 +237,7 @@ describe('TemplateCard', () => { it('is keyboard focusable', () => { render( @@ -203,7 +251,7 @@ describe('TemplateCard', () => { it('renders with long template name without breaking', () => { const longNameTemplate: Template = { - ...mockTemplate, + ...mockBaseTemplate, name: 'This is a very long template name that should be truncated properly', }; @@ -224,7 +272,7 @@ describe('TemplateCard', () => { it('renders with long description without breaking', () => { const longDescTemplate: Template = { - ...mockTemplate, + ...mockUserTemplate, description: 'This is a very long description that goes on and on and should be clamped to two lines using the line-clamp-2 utility class', }; From e0ddfc4cdf6304537028fd86b5d4d837e4aa42d7 Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Fri, 19 Dec 2025 17:06:14 +0100 Subject: [PATCH 3/7] update URLs --- assets/js/utils/editorUrlConversion.ts | 2 +- lib/lightning_web/live/workflow_live/dashboard_components.ex | 2 +- lib/lightning_web/live/workflow_live/helpers.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/js/utils/editorUrlConversion.ts b/assets/js/utils/editorUrlConversion.ts index 5a9a7ee156..a316f46c6a 100644 --- a/assets/js/utils/editorUrlConversion.ts +++ b/assets/js/utils/editorUrlConversion.ts @@ -230,7 +230,7 @@ export function buildCollaborativeEditorUrl(options: { : ''; const basePath = isNewWorkflow - ? `/projects/${projectId}/w/new/collaborate` + ? `/projects/${projectId}/w/new/collaborate?method=template` : `/projects/${projectId}/w/${workflowId}/collaborate`; return `${basePath}${queryString}`; diff --git a/lib/lightning_web/live/workflow_live/dashboard_components.ex b/lib/lightning_web/live/workflow_live/dashboard_components.ex index 814d7da2de..b479bc9f2f 100644 --- a/lib/lightning_web/live/workflow_live/dashboard_components.ex +++ b/lib/lightning_web/live/workflow_live/dashboard_components.ex @@ -353,7 +353,7 @@ defmodule LightningWeb.WorkflowLive.DashboardComponents do tooltip={@tooltip} phx-click={ if !@disabled, - do: JS.navigate(~p"/projects/#{@project_id}/w/new?method=template") + do: JS.navigate(~p"/projects/#{@project_id}/w/new") } class="col-span-1 w-full" role="button" diff --git a/lib/lightning_web/live/workflow_live/helpers.ex b/lib/lightning_web/live/workflow_live/helpers.ex index 65974ce766..bedb15158b 100644 --- a/lib/lightning_web/live/workflow_live/helpers.ex +++ b/lib/lightning_web/live/workflow_live/helpers.ex @@ -447,7 +447,7 @@ defmodule LightningWeb.WorkflowLive.Helpers do end defp collaborative_base_url(%{"project_id" => project_id}, :new) do - "/projects/#{project_id}/w/new/collaborate" + "/projects/#{project_id}/w/new/collaborate?method=template" end defp collaborative_base_url(%{"id" => id, "project_id" => project_id}, :edit) do From 1a11085dfd655fa94c653928e9d85b65f36a878a Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Fri, 19 Dec 2025 17:10:35 +0100 Subject: [PATCH 4/7] cleaner urls --- lib/lightning_web/live/workflow_live/helpers.ex | 2 +- test/lightning_web/live/workflow_live/collaborate_new_test.exs | 2 +- test/lightning_web/live/workflow_live/helpers_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/lightning_web/live/workflow_live/helpers.ex b/lib/lightning_web/live/workflow_live/helpers.ex index bedb15158b..f1cac6d905 100644 --- a/lib/lightning_web/live/workflow_live/helpers.ex +++ b/lib/lightning_web/live/workflow_live/helpers.ex @@ -380,7 +380,7 @@ defmodule LightningWeb.WorkflowLive.Helpers do iex> collaborative_editor_url(%{ ...> "project_id" => "proj-1" ...> }, :new) - "/projects/proj-1/w/new/collaborate" + "/projects/proj-1/w/new/collaborate&method=template" # With multiple query params iex> collaborative_editor_url(%{ diff --git a/test/lightning_web/live/workflow_live/collaborate_new_test.exs b/test/lightning_web/live/workflow_live/collaborate_new_test.exs index 08d86f9371..8226db39c8 100644 --- a/test/lightning_web/live/workflow_live/collaborate_new_test.exs +++ b/test/lightning_web/live/workflow_live/collaborate_new_test.exs @@ -24,7 +24,7 @@ defmodule LightningWeb.WorkflowLive.CollaborateNewTest do {:ok, _view, html} = live( conn, - ~p"/projects/#{sandbox.id}/w/new/collaborate" + ~p"/projects/#{sandbox.id}/w/new/collaborate&method=template" ) assert html =~ "data-root-project-id=\"#{parent_project.id}\"" diff --git a/test/lightning_web/live/workflow_live/helpers_test.exs b/test/lightning_web/live/workflow_live/helpers_test.exs index d2308a5d68..06f73cb0c3 100644 --- a/test/lightning_web/live/workflow_live/helpers_test.exs +++ b/test/lightning_web/live/workflow_live/helpers_test.exs @@ -10,7 +10,7 @@ defmodule LightningWeb.WorkflowLive.HelpersTest do } result = Helpers.collaborative_editor_url(params, :new) - assert result == "/projects/proj-1/w/new/collaborate" + assert result == "/projects/proj-1/w/new/collaborate?method=template" end test "converts classical editor URL to collaborative for existing workflow" do From 77a247031d4a663d088a7fe70f84910ab836f1ce Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Fri, 19 Dec 2025 17:19:17 +0100 Subject: [PATCH 5/7] update tests --- assets/test/utils/editorUrlConversion.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/test/utils/editorUrlConversion.test.ts b/assets/test/utils/editorUrlConversion.test.ts index dce5b13a81..c9fcf787e3 100644 --- a/assets/test/utils/editorUrlConversion.test.ts +++ b/assets/test/utils/editorUrlConversion.test.ts @@ -244,7 +244,9 @@ describe('editorUrlConversion', () => { isNewWorkflow: true, }); - expect(url).toBe('/projects/proj-123/w/new/collaborate?job=job-abc'); + expect(url).toBe( + '/projects/proj-123/w/new/collaborate?method=template?job=job-abc' + ); }); it('builds URL without query params when empty', () => { From fbd44c5bae82284d528a69d759d6ac5b0de4861a Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Fri, 19 Dec 2025 17:30:10 +0100 Subject: [PATCH 6/7] fix test --- test/lightning_web/live/workflow_live/collaborate_new_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lightning_web/live/workflow_live/collaborate_new_test.exs b/test/lightning_web/live/workflow_live/collaborate_new_test.exs index 8226db39c8..5b02b32e17 100644 --- a/test/lightning_web/live/workflow_live/collaborate_new_test.exs +++ b/test/lightning_web/live/workflow_live/collaborate_new_test.exs @@ -24,7 +24,7 @@ defmodule LightningWeb.WorkflowLive.CollaborateNewTest do {:ok, _view, html} = live( conn, - ~p"/projects/#{sandbox.id}/w/new/collaborate&method=template" + ~p"/projects/#{sandbox.id}/w/new/collaborate?method=template" ) assert html =~ "data-root-project-id=\"#{parent_project.id}\"" From 9eb650cee738f21baa19c4e799cf0ad1a0a53a4f Mon Sep 17 00:00:00 2001 From: Taylor Downs Date: Fri, 19 Dec 2025 21:04:06 +0100 Subject: [PATCH 7/7] handle param building --- assets/js/utils/editorUrlConversion.ts | 11 +++++++---- assets/test/utils/editorUrlConversion.test.ts | 2 +- lib/lightning_web/live/workflow_live/helpers.ex | 6 ++++-- .../live/workflow_live/helpers_test.exs | 14 ++++++++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/assets/js/utils/editorUrlConversion.ts b/assets/js/utils/editorUrlConversion.ts index a316f46c6a..218634fb4d 100644 --- a/assets/js/utils/editorUrlConversion.ts +++ b/assets/js/utils/editorUrlConversion.ts @@ -224,15 +224,18 @@ export function buildCollaborativeEditorUrl(options: { const collaborativeParams = classicalToCollaborativeParams(searchParams, { selectedType, }); - const queryString = - collaborativeParams.toString().length > 0 - ? `?${collaborativeParams.toString()}` - : ''; const basePath = isNewWorkflow ? `/projects/${projectId}/w/new/collaborate?method=template` : `/projects/${projectId}/w/${workflowId}/collaborate`; + // If base URL already has query params, use & instead of ? + const hasExistingParams = basePath.includes('?'); + const queryString = + collaborativeParams.toString().length > 0 + ? `${hasExistingParams ? '&' : '?'}${collaborativeParams.toString()}` + : ''; + return `${basePath}${queryString}`; } diff --git a/assets/test/utils/editorUrlConversion.test.ts b/assets/test/utils/editorUrlConversion.test.ts index c9fcf787e3..b619f8a8bf 100644 --- a/assets/test/utils/editorUrlConversion.test.ts +++ b/assets/test/utils/editorUrlConversion.test.ts @@ -245,7 +245,7 @@ describe('editorUrlConversion', () => { }); expect(url).toBe( - '/projects/proj-123/w/new/collaborate?method=template?job=job-abc' + '/projects/proj-123/w/new/collaborate?method=template&job=job-abc' ); }); diff --git a/lib/lightning_web/live/workflow_live/helpers.ex b/lib/lightning_web/live/workflow_live/helpers.ex index f1cac6d905..89ce51eec1 100644 --- a/lib/lightning_web/live/workflow_live/helpers.ex +++ b/lib/lightning_web/live/workflow_live/helpers.ex @@ -380,7 +380,7 @@ defmodule LightningWeb.WorkflowLive.Helpers do iex> collaborative_editor_url(%{ ...> "project_id" => "proj-1" ...> }, :new) - "/projects/proj-1/w/new/collaborate&method=template" + "/projects/proj-1/w/new/collaborate?method=template" # With multiple query params iex> collaborative_editor_url(%{ @@ -460,6 +460,8 @@ defmodule LightningWeb.WorkflowLive.Helpers do defp build_url_with_params(base_url, params) do query_string = URI.encode_query(params) - "#{base_url}?#{query_string}" + # Use & if base_url already has query params, otherwise use ? + separator = if String.contains?(base_url, "?"), do: "&", else: "?" + "#{base_url}#{separator}#{query_string}" end end diff --git a/test/lightning_web/live/workflow_live/helpers_test.exs b/test/lightning_web/live/workflow_live/helpers_test.exs index 06f73cb0c3..fd7c0faaf1 100644 --- a/test/lightning_web/live/workflow_live/helpers_test.exs +++ b/test/lightning_web/live/workflow_live/helpers_test.exs @@ -13,6 +13,20 @@ defmodule LightningWeb.WorkflowLive.HelpersTest do assert result == "/projects/proj-1/w/new/collaborate?method=template" end + test "converts new workflow URL with additional params using & separator" do + params = %{ + "project_id" => "proj-1", + "s" => "job-abc", + "v" => "42" + } + + result = Helpers.collaborative_editor_url(params, :new) + # Should use & to append params since base URL already has ?method=template + assert result =~ "/projects/proj-1/w/new/collaborate?method=template&" + assert result =~ "job=job-abc" + assert result =~ "v=42" + end + test "converts classical editor URL to collaborative for existing workflow" do params = %{ "project_id" => "proj-1",