From 864e5f02ec1c2dd16c74524f551f44485df10a20 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Sat, 12 Sep 2026 08:55:12 +0200 Subject: [PATCH] fix: bring back Publish to Hub for scripts (#11097) * fix: bring back Publish to Hub for scripts Publishing to the Hub moved to the folder-level flow, which publishes a whole project and needs a workspace admin. That left no way to share a single script, which is what private hubs mostly use the Hub for. Restore the "Publish to Hub" item on the script detail page and in the script list row menu. Both open the Hub's script submission form prefilled with the script, on whichever Hub the instance is configured to use, and are hidden when the instance disables the Hub. Flows and apps still reach the Hub only inside a project. Co-Authored-By: Claude Opus 5 (1M context) * fix: open the Hub tab before fetching, and hide Publish to Hub from operators The script list row has to fetch the script before it can build the Hub URL, and Safari refuses window.open after an await, so the tab never opened there. Claim it inside the click with claimTab(), point it at the Hub once the script loads, and close it with a toast if the fetch fails. A blocked popup falls back to a late window.open, and says so if that is blocked too. Operators can't write scripts, so the row menu now hides the item from them, as the script page's menu already does. The script page opens the Hub with noopener, and scriptToHubUrl takes the script instead of eight positional arguments. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- .../components/common/table/ScriptRow.svelte | 43 ++++++++++++++++++- frontend/src/lib/hub.ts | 29 ++++++++++++- .../scripts/get/[...hash]/+page.svelte | 22 +++++++++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/common/table/ScriptRow.svelte b/frontend/src/lib/components/common/table/ScriptRow.svelte index de0a327b1b..ab5a460a5e 100644 --- a/frontend/src/lib/components/common/table/ScriptRow.svelte +++ b/frontend/src/lib/components/common/table/ScriptRow.svelte @@ -9,7 +9,14 @@ import type ShareModal from '$lib/components/ShareModal.svelte' import { ScriptService, type Script } from '$lib/gen' - import { userStore, userWorkspaces, workspaceStore } from '$lib/stores' + import { + disableHubStore, + hubBaseUrlStore, + userStore, + userWorkspaces, + workspaceStore + } from '$lib/stores' + import { scriptToHubUrl } from '$lib/hub' import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte' import { createEventDispatcher } from 'svelte' @@ -32,6 +39,7 @@ FolderOpen, ChevronUpSquare, GitFork, + Globe2, List, Pen, Shield, @@ -47,7 +55,12 @@ import Popover from '$lib/components/Popover.svelte' import Tooltip from '$lib/components/Tooltip.svelte' import { getDeployUiSettings } from '$lib/components/home/deploy_ui' - import { editInForkAllowed, editInForkLabel, onEditInForkClick } from '$lib/utils/editInFork' + import { + claimTab, + editInForkAllowed, + editInForkLabel, + onEditInForkClick + } from '$lib/utils/editInFork' import EditInForkButton from './EditInForkButton.svelte' import { isCloudHosted } from '$lib/cloud' @@ -403,6 +416,32 @@ copyToClipboard(script.path) } }, + { + displayName: 'Publish to Hub', + icon: Globe2, + action: async () => { + // The row only carries metadata, so the code has to be fetched first; the tab is + // claimed before that, since Safari won't open one after an await. + const tab = claimTab() + try { + const fullScript = await ScriptService.getScriptByPath({ + workspace: $workspaceStore!, + path: script.path + }) + const url = scriptToHubUrl(fullScript, $hubBaseUrlStore).toString() + if (tab) { + tab.show(url) + } else if (!window.open(url)) { + sendUserToast('Allow popups to publish this script to the Hub', true) + } + } catch (e: any) { + tab?.discard() + sendUserToast(`Could not load ${script.path}: ${e?.body ?? e?.message ?? e}`, true) + } + }, + // Operators can't write scripts, so they have nothing to publish. + hide: $disableHubStore || $userStore?.operator + }, { displayName: script.archived ? 'Unarchive' : 'Archive', icon: Archive, diff --git a/frontend/src/lib/hub.ts b/frontend/src/lib/hub.ts index 25b3f8b3fd..9f81614258 100644 --- a/frontend/src/lib/hub.ts +++ b/frontend/src/lib/hub.ts @@ -1,5 +1,6 @@ -import { AppService, FlowService } from './gen' +import { AppService, FlowService, type Script } from './gen' import hubPathsData from './hubPaths.json' +import { encodeState } from './utils' import { replacePlaceholderForSignatureScriptTemplate, SIGNATURE_TEMPLATE_FLOW_HUB_ID, @@ -17,6 +18,32 @@ export const HubFlow = { SIGNATURE_TEMPLATE: SIGNATURE_TEMPLATE_FLOW_HUB_ID } as const +/** + * The Hub's script submission form, prefilled with this script. The Hub decodes the + * hash, so the code never reaches its server until the user submits. Flows and apps + * reach the Hub inside a project, published from a folder, so only scripts have this. + */ +export function scriptToHubUrl( + script: Pick< + Script, + 'content' | 'summary' | 'description' | 'kind' | 'language' | 'schema' | 'lock' + >, + hubBaseUrl: string +): URL { + const { content, summary, kind, language, schema } = script + const url = new URL(hubBaseUrl + '/scripts/add') + url.hash = encodeState({ + content, + summary, + description: script.description ?? '', + kind, + language, + schema, + lock: script.lock ?? '' + }) + return url +} + export function replaceScriptPlaceholderWithItsValues(id: string, content: string) { switch (id) { case HubScript.SIGNATURE_TEMPLATE: diff --git a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte index f0023ddb49..a856c9d0ac 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -22,7 +22,14 @@ } from '$lib/utils' import Tooltip from '$lib/components/Tooltip.svelte' import ShareModal from '$lib/components/ShareModal.svelte' - import { enterpriseLicense, userStore, userWorkspaces, workspaceStore } from '$lib/stores' + import { + disableHubStore, + enterpriseLicense, + hubBaseUrlStore, + userStore, + userWorkspaces, + workspaceStore + } from '$lib/stores' import { isDeployable, ALL_DEPLOYABLE } from '$lib/utils_deployable' import AIFormAssistant from '$lib/components/copilot/AIFormAssistant.svelte' @@ -59,6 +66,7 @@ Eye, FolderOpen, GitFork, + Globe2, History, Loader2, Pen, @@ -71,6 +79,7 @@ ChevronDown, ChevronRight } from 'lucide-svelte' + import { scriptToHubUrl } from '$lib/hub' import SharedBadge from '$lib/components/SharedBadge.svelte' import Popover from '$lib/components/Popover.svelte' import ScriptVersionHistory from '$lib/components/ScriptVersionHistory.svelte' @@ -619,6 +628,17 @@ }) } + if (!$disableHubStore) { + menuItems.push({ + label: 'Publish to Hub', + Icon: Globe2, + onclick: () => { + if (!script) return + window.open(scriptToHubUrl(script, $hubBaseUrlStore).toString(), '_blank', 'noopener') + } + }) + } + if (showEditButtons) { if (script.archived) { menuItems.push({