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({