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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-12 08:55:12 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 670628b300
commit 864e5f02ec
3 changed files with 90 additions and 4 deletions
@@ -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,
+28 -1
View File
@@ -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:
@@ -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({