feat: Add image when publishing a project (#10310)

* refactor(hub): remove per-item Publish to Hub entry points

Publishing to the Hub now happens exclusively through the folder-level
deploy-to-hub flow (/folders). Remove the standalone entry points:

- script detail page menu item (and the SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB
  const that gated it)
- script list row dropdown item
- raw app editor menu item, its zip-download drawer and publishToHub()
- long-dead commented block in AppEditorHeader

Also drop the now-orphaned URL helpers (scriptToHubUrl, flowToHubUrl,
appToHubUrl, rawAppToHubUrl) from lib/hub.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub): upload a custom project logo from the deploy-to-hub drawer

Add a Logo field to the bundle metadata form: a drag-and-drop dropzone
(png/svg, 512KB client-side cap mirrored server-side by the Hub) that
turns into a live replica of the Hub project card once an image is
picked, so the logo can be judged in context before publishing. The
logo is pushed after the draft's items/migrations via the new
POST /projects/{slug}/logo proxy in hub_publish.rs (slug validated by
construction, `logo: null` forwarded to clear). Leaving the field empty
never touches the Hub's existing logo, so re-publishing a bundle keeps it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub): logo removal, safer mime inference, explicit clear semantics

Review follow-ups on the project logo upload:

- Removing a published logo is now possible: hubLogo is three-state
  (undefined = leave the Hub's logo alone, null = clear on publish,
  object = upload). Rehydration reads has_logo so the drawer shows a
  "Remove on publish" affordance when the Hub already has one, with an
  undo banner before publishing.
- hub_publish.rs uses a double-Option for the logo field: a missing
  `logo` key is now a 400 instead of being serialized as `logo: null`,
  which the Hub interprets as an explicit clear — POSTing `{}` can no
  longer silently delete a project's logo.
- Client mime inference prefers the browser-reported file.type over the
  filename extension, so a PNG misnamed *.svg no longer produces a
  broken preview and a guaranteed server-side sniff rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update frontend/src/lib/components/workspaceSettings/deployToHubSession.svelte.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update frontend/src/lib/components/workspaceSettings/DeployToHub.svelte

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix(hub): validate logo size/mime/base64 in the proxy, document the endpoint

- Enforce the logo constraints in windmill-api itself instead of relying
  on the browser and remote Hub: a route-level DefaultBodyLimit sized
  for a max logo in base64 (+JSON envelope) overrides the global request
  limit, and the handler validates the mime allowlist, base64 alphabet
  and decoded length (512KB cap) before anything is forwarded.
- Add /w/{workspace}/hub/projects/{slug}/logo to openapi.yaml (with the
  ProjectLogoBody schema) and regenerate the frontend client.
- Drop a narrating comment on the hidden file input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Tristan TR
2026-07-24 18:19:25 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 cubic-dev-ai[bot]
parent f00fcb2d1b
commit 48618cff8c
10 changed files with 329 additions and 192 deletions
+56
View File
@@ -23313,6 +23313,42 @@ paths:
schema:
type: string
/w/{workspace}/hub/projects/{slug}/logo:
post:
summary: set or clear a hub project's custom logo
description: |
Requires the caller to be a workspace admin. Sets the project's custom
logo (base64 png/svg, decoded size max 512KB) or clears it when `logo`
is null; the `logo` field itself is required so an empty body cannot
clear the logo by accident. Forwards the request to the configured Hub
scoped to the `{workspace}:{folder}` source and returns the Hub's
status code and raw response body.
operationId: publishHubProjectLogo
tags:
- hubPublish
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: slug
in: path
required: true
description: hub project slug
schema:
$ref: "#/components/schemas/HubProjectSlug"
- $ref: "#/components/parameters/HubPublishFolder"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ProjectLogoBody"
responses:
"200":
description: raw Hub response body (status code is passed through from the Hub)
content:
text/plain:
schema:
type: string
/w/{workspace}/hub/projects/{slug}/submit:
post:
summary: submit a hub project draft for review
@@ -32495,6 +32531,26 @@ components:
recording:
type: object
ProjectLogoBody:
type: object
properties:
logo:
description: the logo to set, or null to clear the project's current logo
nullable: true
type: object
properties:
b64:
description: base64-encoded image bytes (decoded size max 512KB)
type: string
mime:
type: string
enum: [image/png, image/svg+xml]
required:
- b64
- mime
required:
- logo
PublishResourceTypeBody:
type: object
properties:
+80 -1
View File
@@ -2,7 +2,7 @@ use crate::auth::Tokened;
use crate::db::ApiAuthed;
use crate::HTTP_CLIENT;
use axum::{
extract::{FromRequestParts, Json, Path, Query, RawPathParams},
extract::{DefaultBodyLimit, FromRequestParts, Json, Path, Query, RawPathParams},
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
routing::{get, post},
@@ -32,6 +32,10 @@ pub fn workspaced_service() -> Router {
"/projects/{slug}/pipeline_recording",
post(publish_pipeline_recording),
)
.route(
"/projects/{slug}/logo",
post(publish_project_logo).layer(DefaultBodyLimit::max(LOGO_BODY_LIMIT)),
)
.route("/resource_types", post(publish_resource_type))
.route("/resources", post(publish_resources))
.route("/triggers", post(publish_triggers))
@@ -359,6 +363,81 @@ async fn publish_pipeline_recording(
.await
}
#[derive(Deserialize, Serialize)]
struct ProjectLogoInner {
b64: String,
mime: String,
}
// Custom project logo (png/svg, base64). Double-Option so a missing `logo`
// key is distinguishable from an explicit `logo: null` (which clears the
// logo on the Hub) — otherwise POSTing `{}` would silently delete it.
#[derive(Deserialize, Serialize)]
struct ProjectLogoBody {
#[serde(default, deserialize_with = "deserialize_explicit")]
logo: Option<Option<ProjectLogoInner>>,
}
fn deserialize_explicit<'de, D: Deserializer<'de>>(
d: D,
) -> Result<Option<Option<ProjectLogoInner>>, D::Error> {
Option::<ProjectLogoInner>::deserialize(d).map(Some)
}
// Mirrors the Hub's own limits so an oversized/invalid payload is rejected
// here instead of being deserialized, copied and forwarded first. The route
// also carries a DefaultBodyLimit sized for a max logo in base64 + JSON
// envelope, overriding the much larger global request limit.
const MAX_LOGO_BYTES: usize = 512 * 1024;
const LOGO_BODY_LIMIT: usize = MAX_LOGO_BYTES / 3 * 4 + 16 * 1024;
const ALLOWED_LOGO_MIMES: [&str; 2] = ["image/png", "image/svg+xml"];
fn validate_logo(inner: &ProjectLogoInner) -> Result<(), Error> {
if !ALLOWED_LOGO_MIMES.contains(&inner.mime.as_str()) {
return Err(Error::BadRequest(
"logo mime must be image/png or image/svg+xml".to_string(),
));
}
let b = inner.b64.as_bytes();
let padding = b.iter().rev().take_while(|&&c| c == b'=').count();
let valid = !b.is_empty()
&& b.len() % 4 == 0
&& padding <= 2
&& b[..b.len() - padding]
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'+' || c == b'/');
if !valid {
return Err(Error::BadRequest("invalid base64".to_string()));
}
if b.len() / 4 * 3 - padding > MAX_LOGO_BYTES {
return Err(Error::BadRequest(format!(
"logo too large (max {}KB)",
MAX_LOGO_BYTES / 1024
)));
}
Ok(())
}
async fn publish_project_logo(
ctx: HubPublishCtx,
Path((_workspace, slug)): Path<(String, ProjectSlug)>,
Json(body): Json<ProjectLogoBody>,
) -> Result<impl IntoResponse, Error> {
let Some(logo) = body.logo else {
return Err(Error::BadRequest(
"logo field is required: an object to set it, or null to clear it".to_string(),
));
};
if let Some(inner) = &logo {
validate_logo(inner)?;
}
ctx.post(
&format!("/projects/{}/logo", slug),
&serde_json::json!({ "logo": logo }),
)
.await
}
#[derive(Deserialize, Serialize)]
struct PublishResourceTypeBody {
name: String,
@@ -599,15 +599,6 @@
window.open(computeSecretUrl(secretUrl), '_blank')
}
},
// {
// displayName: 'Publish to Hub',
// icon: faGlobe,
// action: () => {
// const url = appToHubUrl(toStatic($app, $staticExporter, $summary, $hubBaseUrlStore))
// window.open(url.toString(), '_blank')
// }
// },
{
displayName: 'App inputs',
icon: FormInput,
@@ -9,7 +9,7 @@
import type ShareModal from '$lib/components/ShareModal.svelte'
import { ScriptService, type Script } from '$lib/gen'
import { hubBaseUrlStore, userStore, userWorkspaces, workspaceStore } from '$lib/stores'
import { userStore, userWorkspaces, workspaceStore } from '$lib/stores'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import { createEventDispatcher } from 'svelte'
@@ -36,7 +36,6 @@
Shield,
Trash,
History,
Globe2,
FileText
} from 'lucide-svelte'
import ScriptVersionHistory from '$lib/components/ScriptVersionHistory.svelte'
@@ -47,7 +46,6 @@
import Popover from '$lib/components/Popover.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
import { scriptToHubUrl } from '$lib/hub'
import { buildForkEditUrl, editInForkAllowed, editInForkLabel } from '$lib/utils/editInFork'
import { isCloudHosted } from '$lib/cloud'
@@ -406,29 +404,6 @@
copyToClipboard(script.path)
}
},
{
displayName: 'Publish to Hub',
icon: Globe2,
action: async () => {
const scriptData = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: script.path
})
window.open(
scriptToHubUrl(
scriptData.content,
scriptData.summary,
scriptData.description ?? '',
scriptData.kind,
scriptData.language,
scriptData.schema,
scriptData.lock ?? '',
$hubBaseUrlStore
).toString(),
'_blank'
)
}
},
{
displayName: script.archived ? 'Unarchive' : 'Archive',
icon: Archive,
@@ -10,22 +10,17 @@
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import OpenInSessionButton from '$lib/components/sessions/OpenInSessionButton.svelte'
import { discardDraftAfterDeploy } from '$lib/userDraftToast'
import { rawAppToHubUrl } from '$lib/hub'
import {
enterpriseLicense,
hubBaseUrlStore,
userStore,
userWorkspaces,
workspaceStore
} from '$lib/stores'
import YAML from 'yaml'
import {
Bug,
DiffIcon,
Download,
EllipsisVertical,
FileJson,
Globe,
History,
PanelLeft,
PanelLeftClose,
@@ -295,8 +290,6 @@
let saveDrawerOpen = $state(false)
let historyBrowserDrawerOpen = $state(false)
let publishToHubDrawerOpen = $state(false)
let publishingToHub = $state(false)
let deploymentMsg: string | undefined = $state(undefined)
// Top-bar responsive collapse — container width, not viewport.
@@ -307,37 +300,6 @@
// smallest well-supported unified size (`sm`) so the bar is thinner.
const headerBtnSize = $derived(condensedHeader ? 'sm' : 'md')
async function publishToHub() {
if (!app) return
publishingToHub = true
try {
const { default: JSZip } = await import('jszip')
const { js, css } = await getBundle()
const zip = new JSZip()
zip.file('app.yaml', YAML.stringify(app))
zip.file('bundle.js', js)
zip.file('bundle.css', css)
const blob = await zip.generateAsync({ type: 'blob' })
// Download the zip
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${(appPath || 'raw-app').replaceAll('/', '__')}.zip`
a.click()
setTimeout(() => URL.revokeObjectURL(url), 100)
// Open hub page
const hubUrl = rawAppToHubUrl(
$hubBaseUrlStore,
summary || appPath.split('/').pop()?.replace('_', ' ') || 'my raw app'
)
window.open(hubUrl.toString(), '_blank')
} finally {
publishingToHub = false
}
}
function closeSaveDrawer() {
saveDrawerOpen = false
}
@@ -615,13 +577,6 @@
displayName: 'Edit in YAML',
icon: FileJson,
action: () => onOpenYamlEditor?.()
},
{
displayName: 'Publish to Hub',
icon: Globe,
action: () => {
publishToHubDrawerOpen = true
}
}
])
@@ -744,42 +699,6 @@
</DrawerContent>
</Drawer>
<Drawer bind:open={publishToHubDrawerOpen} size="600px">
<DrawerContent title="Publish to Hub" on:close={() => (publishToHubDrawerOpen = false)}>
{#snippet actions()}
<Button
loading={publishingToHub}
disabled={!app}
on:click={publishToHub}
variant="accent"
startIcon={{ icon: Download }}
>
Download & open hub
</Button>
{/snippet}
<div class="flex flex-col gap-4">
<p class="text-secondary text-sm">
This will download a zip file containing your raw app bundle and open the Windmill Hub
submission page.
</p>
<div class="text-sm">
<p class="font-semibold mb-2">The zip file will contain:</p>
<ul class="list-disc list-inside text-secondary space-y-1">
<li
><code class="text-xs bg-surface-secondary px-1 rounded">app.yaml</code> - App configuration</li
>
<li
><code class="text-xs bg-surface-secondary px-1 rounded">bundle.js</code> - JavaScript bundle</li
>
<li
><code class="text-xs bg-surface-secondary px-1 rounded">bundle.css</code> - CSS styles</li
>
</ul>
</div>
</div>
</DrawerContent>
</Drawer>
<AppJobsDrawer
bind:open={jobsDrawerOpen}
on:clear={() => {
@@ -31,6 +31,7 @@
Eye,
ExternalLink,
Globe,
Image as ImageIcon,
Info,
LayoutDashboard,
Loader2,
@@ -99,6 +100,56 @@
async function confirmPublish() {
if (await deployHub.session?.confirmPublish()) publishDrawer?.closeDrawer()
}
// Client-side mirror of the Hub's logo constraints (it re-validates server-side).
const MAX_LOGO_BYTES = 512 * 1024
let logoDragOver = $state(false)
let logoFileInput = $state<HTMLInputElement | undefined>()
async function handleLogoFile(file: File | undefined) {
const s = deployHub.session
if (!s || !file) return
// Browser-reported type wins over the extension: a PNG misnamed *.svg
// must be treated as PNG or the Hub's content sniff rejects it later.
const lower = file.name.toLowerCase()
const mime =
file.type
? file.type === 'image/png'
? 'image/png'
: file.type === 'image/svg+xml'
? 'image/svg+xml'
: undefined
: lower.endsWith('.png')
? 'image/png'
: lower.endsWith('.svg')
? 'image/svg+xml'
: undefined
if (!mime) {
sendUserToast('Logo must be a PNG or SVG file', true)
return
}
if (file.size > MAX_LOGO_BYTES) {
sendUserToast('Logo too large (max 512KB)', true)
return
}
const buf = new Uint8Array(await file.arrayBuffer())
let bin = ''
for (let i = 0; i < buf.length; i += 0x8000) {
bin += String.fromCharCode(...buf.subarray(i, i + 0x8000))
}
s.hubLogo = { b64: btoa(bin), mime, name: file.name }
}
async function onLogoPicked(e: Event) {
const input = e.currentTarget as HTMLInputElement
const file = input.files?.[0]
// Reset so re-picking the same file re-fires `change`.
input.value = ''
await handleLogoFile(file)
}
async function onLogoDrop(e: DragEvent) {
e.preventDefault()
logoDragOver = false
await handleLogoFile(e.dataTransfer?.files?.[0])
}
async function copyIframeSnippet(url: string) {
const snippet = `<iframe src="${url}" width="100%" height="600" frameborder="0"></iframe>`
try {
@@ -1114,6 +1165,121 @@
inputProps={{ placeholder: 'Short one-liner shown on the Hub card' }}
/>
</label>
<div class="flex flex-col gap-1 text-xs">
<span class="font-semibold text-primary">{s.hubLogo ? 'Preview' : 'Logo'}</span>
{#if s.hubLogo}
<!-- Live replica of the Hub project card so the user can judge the
logo in its real context before publishing. -->
<span class="text-[11px] text-hint">
This is how your project card will look on the Hub.
</span>
<div class="mt-1 flex flex-col items-center gap-2">
<div class="w-64 overflow-hidden rounded-2xl border shadow-sm">
<div
class="flex h-32 items-center justify-center bg-surface-secondary/50 px-4"
>
<img
src={`data:${s.hubLogo.mime};base64,${s.hubLogo.b64}`}
alt="Project logo preview"
class="max-h-16 max-w-36 object-contain"
/>
</div>
<div class="border-t bg-surface p-3">
<div class="truncate text-xs font-medium text-primary">
{s.hubName.trim() || 'Project name'}
</div>
<p class="mt-0.5 line-clamp-2 text-[11px] text-secondary">
{s.hubSummary.trim() || s.hubName.trim() || 'Short one-liner shown on the Hub card'}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<Button
size="xs"
variant="default"
startIcon={{ icon: ImageIcon }}
onclick={() => logoFileInput?.click()}
>
Replace
</Button>
<Button
size="xs"
variant="default"
startIcon={{ icon: X }}
onclick={() => (s.hubLogo = undefined)}
>
Remove
</Button>
</div>
</div>
{:else}
{#if s.hubLogo === null}
<div
class="flex items-center justify-between gap-2 rounded border border-orange-300 bg-orange-50 px-3 py-2 dark:border-orange-800 dark:bg-orange-950/30"
>
<span class="text-orange-700 dark:text-orange-300">
The project's current logo will be removed when you publish.
</span>
<Button size="xs" variant="default" onclick={() => (s.hubLogo = undefined)}>
Undo
</Button>
</div>
{:else if s.hubHasRemoteLogo}
<div
class="flex items-center justify-between gap-2 rounded border bg-surface-secondary/50 px-3 py-2"
>
<span class="text-secondary">
This project already has a custom logo on the Hub.
</span>
<Button
size="xs"
variant="default"
startIcon={{ icon: X }}
onclick={() => (s.hubLogo = null)}
>
Remove on publish
</Button>
</div>
{/if}
<button
type="button"
class={`group flex w-full cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed px-4 py-6 text-center transition-colors ${
logoDragOver
? 'border-blue-400 bg-blue-50 dark:bg-blue-950/30'
: 'text-secondary hover:border-blue-300 hover:bg-surface-secondary/50'
}`}
onclick={() => logoFileInput?.click()}
ondragover={(e) => {
e.preventDefault()
logoDragOver = true
}}
ondragleave={() => (logoDragOver = false)}
ondrop={onLogoDrop}
>
<ImageIcon
size={20}
class="text-tertiary transition-colors group-hover:text-secondary"
/>
<span class="font-medium text-primary">
Drop an image or <span class="text-blue-500">browse</span>
</span>
<span class="text-[11px] text-hint">PNG or SVG, max 512KB</span>
</button>
{/if}
<input
bind:this={logoFileInput}
type="file"
accept=".png,.svg,image/png,image/svg+xml"
style="display: none"
onchange={onLogoPicked}
/>
{#if s.hubLogo === undefined}
<span class="text-[11px] text-hint">
Optional. Shown on the Hub project card and page. Leaving it empty keeps the
project's current logo.
</span>
{/if}
</div>
<label class="flex flex-col gap-1 text-xs">
<span class="font-semibold text-primary">Readme</span>
<textarea
@@ -234,6 +234,14 @@ export class DeployToHubSession {
hubName = $state('')
hubSummary = $state('')
hubReadme = $state('')
// Custom logo state for the next publish (png/svg, base64 without the
// data: prefix). Three-state: undefined = untouched (publishing leaves the
// Hub's current logo alone), null = clear the Hub's logo on publish,
// object = upload this image.
hubLogo = $state<{ b64: string; mime: string; name: string } | null | undefined>(undefined)
// Whether the Hub currently has a custom logo for this project (from
// rehydration) — drives the "Remove current logo" affordance.
hubHasRemoteLogo = $state(false)
effectiveSlug = $state('')
hubItemIds = $state<Record<string, number>>({})
@@ -586,6 +594,7 @@ export class DeployToHubSession {
this.hubName = p.name ?? ''
this.hubSummary = p.summary ?? ''
this.hubReadme = p.readme ?? ''
this.hubHasRemoteLogo = p.has_logo === true
this.phase =
p.status === 'live' ? 'live' : p.status === 'under_review' ? 'under_review' : 'draft'
const ids: Record<string, number> = {}
@@ -1242,6 +1251,22 @@ export class DeployToHubSession {
failures++
}
// Push the logo only when touched this session: an object uploads it,
// null clears the Hub's current logo, undefined leaves it alone
// (re-publishing a bundle must not clear it).
if (this.hubLogo !== undefined) {
try {
await this.#postHub(`/hub/projects/${encodeURIComponent(slug)}/logo`, {
logo: this.hubLogo ? { b64: this.hubLogo.b64, mime: this.hubLogo.mime } : null
})
this.hubHasRemoteLogo = this.hubLogo !== null
this.hubLogo = undefined
} catch (e: any) {
sendUserToast(`Logo ${this.hubLogo ? 'upload' : 'removal'} failed: ${e?.message ?? e}`, true)
failures++
}
}
await sleep(150)
if (this.#disposed) return
// An incomplete push must never become submittable: a failed transitive item
-2
View File
@@ -22,8 +22,6 @@ export const SIDEBAR_SHOW_SCHEDULES = true
export const WORKSPACE_SHOW_SLACK_CMD = true
export const WORKSPACE_SHOW_WEBHOOK_CLI_SYNC = true
export const SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB = true
export const SCRIPT_VIEW_SHOW_SCHEDULE = true
export const SCRIPT_VIEW_SHOW_EXAMPLE_CURL = true
+1 -45
View File
@@ -1,6 +1,4 @@
import type { Schema } from './common'
import { AppService, FlowService, type Flow, type Script } from './gen'
import { encodeState } from './utils'
import { AppService, FlowService } from './gen'
import hubPathsData from './hubPaths.json'
import {
replacePlaceholderForSignatureScriptTemplate,
@@ -11,22 +9,6 @@ import {
export const DEFAULT_HUB_BASE_URL = 'https://hub.windmill.dev'
export const PRIVATE_HUB_MIN_VERSION = 10_000_000
export function scriptToHubUrl(
content: string,
summary: string,
description: string,
kind: Script['kind'],
language: Script['language'],
schema: Schema | any,
lock: string | undefined,
hubBaseUrl: string
): URL {
const url = new URL(hubBaseUrl + '/scripts/add')
url.hash = encodeState({ content, summary, description, kind, language, schema, lock })
return url
}
export const HubScript = {
SIGNATURE_TEMPLATE: SIGNATURE_TEMPLATE_SCRIPT_HUB_PATH
} as const
@@ -65,32 +47,6 @@ export async function loadHubApps() {
}
}
export function flowToHubUrl(flow: Flow, hubBaseUrl: string): URL {
const url = new URL(hubBaseUrl + '/flows/add')
const openFlow = {
value: flow.value,
summary: flow.summary,
description: flow.description,
schema: flow.schema
}
url.searchParams.append('flow', encodeState(openFlow))
return url
}
export function appToHubUrl(staticApp: any, hubBaseUrl: string): URL {
const url = new URL(hubBaseUrl + '/apps/add')
url.searchParams.append('app', encodeState(staticApp))
return url
}
export function rawAppToHubUrl(hubBaseUrl: string, summary?: string): URL {
const url = new URL(hubBaseUrl + '/raw_apps/add')
if (summary) {
url.searchParams.append('summary', summary)
}
return url
}
type HubPaths = {
gitSyncTest: string
gitInitRepo: string
@@ -24,7 +24,6 @@
import ShareModal from '$lib/components/ShareModal.svelte'
import {
enterpriseLicense,
hubBaseUrlStore,
userStore,
userWorkspaces,
workspaceStore
@@ -63,7 +62,6 @@
Eye,
FolderOpen,
GitFork,
Globe2,
History,
Loader2,
Pen,
@@ -76,8 +74,6 @@
ChevronDown,
ChevronRight
} from 'lucide-svelte'
import { SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB } from '$lib/consts'
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'
@@ -550,30 +546,6 @@
})
}
if (SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB) {
menuItems.push({
label: 'Publish to Hub',
Icon: Globe2,
onclick: () => {
if (!script) return
window.open(
scriptToHubUrl(
script.content,
script.summary,
script.description ?? '',
script.kind,
script.language,
script.schema,
script.lock ?? '',
$hubBaseUrlStore
).toString(),
'_blank'
)
}
})
}
if (showEditButtons) {
if (script.archived) {
menuItems.push({