mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-27 08:02:39 +00:00
fix: fork a raw app into the raw app builder, in its session when there (#11978)
* fix: fork a raw app into the raw app builder, in its session when there Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep the source's sandbox and sdk scopes on a forked raw app Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: fork a raw app from the editor's content, not the server's copy Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
79fe8ab03f
commit
3d6a2bcef2
@@ -1326,7 +1326,7 @@
|
||||
{
|
||||
label: 'Fork',
|
||||
onClick: () => {
|
||||
window.open(`/apps/add?template=${appPath}`)
|
||||
window.open(`/apps/add?template=${encodeURIComponent($appPath)}`)
|
||||
}
|
||||
},
|
||||
...(!isCloudHosted() && editInForkAllowed($workspaceStore, $userWorkspaces)
|
||||
|
||||
@@ -173,6 +173,8 @@
|
||||
// Threaded to the AutosaveIndicator's "Reset to deployed" button.
|
||||
/** `Exit & see details`: see RawAppEditorHeader. */
|
||||
onDetails?: (e: { path: string }) => void
|
||||
/** See RawAppEditorHeader. */
|
||||
onOpenFork?: (path: string) => void
|
||||
onResetToDeployed?: () => void | Promise<void>
|
||||
/** The app_version the draft forked from, for the deploy-time "new version
|
||||
* deployed" guard: deploying is refused with a confirmation while it is not
|
||||
@@ -244,6 +246,7 @@
|
||||
compact = false,
|
||||
pendingDraftPath = $bindable(undefined),
|
||||
onDetails,
|
||||
onOpenFork,
|
||||
onResetToDeployed,
|
||||
loadedFromDraft = false,
|
||||
othersDraftsCount = 0,
|
||||
@@ -2726,6 +2729,7 @@ addEventListener('message', function (e) {
|
||||
{getBundle}
|
||||
{onNavigate}
|
||||
{onDeploy}
|
||||
{onDetails}
|
||||
{onOpenFork}
|
||||
{onResetToDeployed}
|
||||
{loadedFromDraft}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
import AppEditorHeaderDeploy from '../apps/editor/AppEditorHeaderDeploy.svelte'
|
||||
import type { Runnable } from './RawAppInlineScriptRunnable.svelte'
|
||||
import { updateRawAppPolicy } from './rawAppPolicy'
|
||||
import { saveRawAppFork } from './forkRawApp.svelte'
|
||||
import { aiChatManager } from '../copilot/chat/AIChatManager.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
@@ -188,6 +189,9 @@
|
||||
/** `Exit & see details`: leaves the editor for the app's deployed page. A session hosts that
|
||||
* page itself, so it flips the tab there rather than navigating out of the session. */
|
||||
onDetails?: (e: { path: string }) => void
|
||||
/** Opens the draft `Fork` saved. Without it, the fork opens in a new browser tab; a
|
||||
* session opens it as one of its own tabs instead. */
|
||||
onOpenFork?: (path: string) => void
|
||||
onResetToDeployed?: () => void | Promise<void>
|
||||
// See ScriptBuilderProps — same semantics for the raw-app editor's
|
||||
// indicator.
|
||||
@@ -239,6 +243,7 @@
|
||||
onDeploy = undefined,
|
||||
pendingDraftPath = $bindable(undefined),
|
||||
onDetails,
|
||||
onOpenFork,
|
||||
onResetToDeployed,
|
||||
loadedFromDraft = false,
|
||||
othersDraftsCount = 0,
|
||||
@@ -257,6 +262,21 @@
|
||||
const opWorkspace = $derived(autosaveWorkspace ?? $operatingWorkspace)
|
||||
const indicatorPath = $derived(autosavePath ?? liveEditorDraftStoragePath)
|
||||
|
||||
async function forkApp() {
|
||||
if (!opWorkspace || !files) return
|
||||
const workspace = opWorkspace
|
||||
// Opened before the save: a tab opened after an await is no longer tied to the
|
||||
// click, and browsers block it as a popup.
|
||||
const tab = onOpenFork ? undefined : window.open('', '_blank')
|
||||
const forkPath = await saveRawAppFork(workspace, { files, runnables, data, policy })
|
||||
if (!forkPath) {
|
||||
tab?.close()
|
||||
return
|
||||
}
|
||||
if (onOpenFork) onOpenFork(forkPath)
|
||||
else if (tab) tab.location.href = `/apps_raw/edit/${forkPath}?workspace=${workspace}`
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const typed = newEditedPath
|
||||
const baseline = savedApp?.path ?? ''
|
||||
@@ -1217,9 +1237,7 @@
|
||||
]),
|
||||
{
|
||||
label: 'Fork',
|
||||
onClick: () => {
|
||||
window.open(`/apps/add?template=${appPath}`)
|
||||
}
|
||||
onClick: forkApp
|
||||
},
|
||||
...(!isCloudHosted() && editInForkAllowed(opWorkspace, $userWorkspaces)
|
||||
? [
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { get } from 'svelte/store'
|
||||
import type { Policy } from '$lib/gen'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
|
||||
import { mintDraftPath } from '$lib/mintDraftPath'
|
||||
import { userPathPrefix } from '$lib/utils'
|
||||
import { random_adj } from '$lib/components/random_positive_adjetive'
|
||||
import type { RawAppDraft } from '$lib/components/sessions/appDraftCodec'
|
||||
import type { RawAppData } from './dataTableRefUtils'
|
||||
|
||||
/**
|
||||
* Saves what the editor holds as a new raw-app draft under a fresh path and returns that
|
||||
* path, or `undefined` when the save failed. The copy is taken from the editor rather than
|
||||
* reloaded from the server, which would miss edits autosave has not written (or never will,
|
||||
* with auto-save off).
|
||||
*/
|
||||
export async function saveRawAppFork(
|
||||
workspace: string,
|
||||
source: {
|
||||
files: Record<string, string>
|
||||
runnables: Record<string, any>
|
||||
data: RawAppData
|
||||
policy: Policy | undefined
|
||||
}
|
||||
): Promise<string | undefined> {
|
||||
const user = get(userStore)
|
||||
const forkPath = mintDraftPath()
|
||||
const draft: RawAppDraft = {
|
||||
summary: '',
|
||||
files: $state.snapshot(source.files),
|
||||
runnables: $state.snapshot(source.runnables),
|
||||
data: $state.snapshot(source.data),
|
||||
// The forker's identity, since the source's would publish as its owner, but the
|
||||
// source's sandbox and SDK scopes: the copied code's SDK calls depend on them.
|
||||
policy: {
|
||||
on_behalf_of: user?.username.includes('@') ? user.username : `u/${user?.username}`,
|
||||
on_behalf_of_email: user?.email,
|
||||
execution_mode: 'publisher',
|
||||
sandbox: source.policy?.sandbox ?? false,
|
||||
frontend_sdk_scopes: $state.snapshot(source.policy?.frontend_sdk_scopes)
|
||||
},
|
||||
// Without it the fork would deploy under its `draft_<uuid>` storage path.
|
||||
draft_path: `${userPathPrefix(user?.username)}${random_adj()}_app`
|
||||
}
|
||||
UserDraft.save('raw_app', forkPath, draft, { workspace })
|
||||
await UserDraft.forcePersist('raw_app', forkPath, { workspace })
|
||||
const saved = UserDraftDbSyncer.getState({ workspace, itemKind: 'raw_app', path: forkPath })
|
||||
if (saved.state === 'failed') {
|
||||
sendUserToast(`Could not save the fork: ${saved.failureMessage ?? 'unknown error'}`, true)
|
||||
return undefined
|
||||
}
|
||||
return forkPath
|
||||
}
|
||||
@@ -100,7 +100,9 @@
|
||||
files: { ...result.files },
|
||||
runnables: {
|
||||
...result.runnables,
|
||||
...(capabilitiesInWorkspace(workspaceId).apps === 'compose' ? {} : structuredClone(STARTER_RUNNABLES))
|
||||
...(capabilitiesInWorkspace(workspaceId).apps === 'compose'
|
||||
? {}
|
||||
: structuredClone(STARTER_RUNNABLES))
|
||||
},
|
||||
data: result.data,
|
||||
policy: result.policy
|
||||
@@ -162,6 +164,13 @@
|
||||
})
|
||||
})
|
||||
|
||||
function openFork(forkPath: string) {
|
||||
runtime.previewTabs.open({
|
||||
type: 'item',
|
||||
item: { kind: 'app', raw_app: true, path: forkPath, summary: '' }
|
||||
})
|
||||
}
|
||||
|
||||
async function reloadDeployed() {
|
||||
await runtime.loadRawApp(workspaceId, path, true, true)
|
||||
}
|
||||
@@ -380,6 +389,7 @@
|
||||
{diffDrawer}
|
||||
{onNavigate}
|
||||
onDetails={onSeeDetails}
|
||||
onOpenFork={openFork}
|
||||
condensedHeader={true}
|
||||
onResetToDeployed={reloadDeployed}
|
||||
onDeploy={(e) => runtime.itemDeployed(workspaceId, 'raw_app', path, e.path)}
|
||||
|
||||
Reference in New Issue
Block a user