mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
feat(apps): add labels input to app editor deploy drawer (#9828)
* feat(apps): add labels input to app editor deploy drawer
The labels feature (c4c9ef5fd) wired LabelsInput into the script, flow,
schedule, resource and variable editors but left the app editor out: it
had no labels state and createApp/updateApp never sent labels, so apps
could not be labeled from the UI despite full backend support.
Thread the deployed app's labels from the edit page through AppEditor
into AppEditorHeader, render LabelsInput in AppEditorHeaderDeploy after
the summary field (matching ScriptBuilder/FlowSettings), and include
labels in the create/update request bodies, the savedApp snapshot, and
the diff/deploy comparison values. The raw-app editor shares the deploy
drawer, so it is wired symmetrically (createAppRaw/updateAppRaw + the
raw page loader) to avoid leaking a non-functional input there.
Fixes WIN-2107
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(apps): drop redundant labels cast in app edit restore path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): include labels in deploy-drawer Diff current value
The Diff button inside the deploy drawer built its current value without
labels, so the approval preview could hide label changes that would be
deployed. (Identified by cubic.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): reset raw-app labels on new-draft seed
The raw-app edit route keeps labels as route-level state and the
?new_draft=true seed-template branch never cleared it. Since the route is
reused across raw-app navigations, opening a labeled raw app then creating
a fresh one could remount RawAppEditor with the previous app's labels and
deploy them via createAppRaw. Reset labels with the other bleed-prevention
resets at the top of the new-draft branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,7 @@
|
||||
path,
|
||||
policy,
|
||||
summary,
|
||||
labels,
|
||||
deployedBaseline = undefined,
|
||||
fromHub = false,
|
||||
diffDrawer = undefined,
|
||||
@@ -886,6 +887,7 @@
|
||||
<AppEditorHeader
|
||||
{newPath}
|
||||
{newApp}
|
||||
{labels}
|
||||
userDraftPath={appDraftPath}
|
||||
{onResetToDeployed}
|
||||
{loadedFromDraft}
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
summary: string
|
||||
policy: any
|
||||
custom_path?: string
|
||||
labels?: string[]
|
||||
}
|
||||
| undefined
|
||||
version?: number | undefined
|
||||
@@ -96,6 +97,8 @@
|
||||
bottomPanelHidden?: boolean
|
||||
newApp: boolean
|
||||
newPath?: string
|
||||
/** Initial labels for the app, threaded from the loaded app data via AppEditor. */
|
||||
labels?: string[]
|
||||
/** URL path the draft is keyed under; empty on `/apps/add` (no draft yet). */
|
||||
userDraftPath?: string
|
||||
onSavedNewAppPath?: (path: string) => void
|
||||
@@ -129,6 +132,7 @@
|
||||
bottomPanelHidden = false,
|
||||
newApp,
|
||||
newPath = '',
|
||||
labels: initialLabels = undefined,
|
||||
userDraftPath = '',
|
||||
onSavedNewAppPath,
|
||||
onShowLeftPanel,
|
||||
@@ -261,7 +265,8 @@
|
||||
policy,
|
||||
deployment_message: deploymentMsg,
|
||||
custom_path: customPath,
|
||||
preserve_on_behalf_of: preserveOnBehalfOf || undefined
|
||||
preserve_on_behalf_of: preserveOnBehalfOf || undefined,
|
||||
labels
|
||||
}
|
||||
})
|
||||
// New path now exists server-side — drop the autocomplete cache so
|
||||
@@ -272,7 +277,8 @@
|
||||
value: structuredClone($state.snapshot($app)),
|
||||
path: path,
|
||||
policy: policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels: $state.snapshot(labels)
|
||||
}
|
||||
closeSaveDrawer()
|
||||
sendUserToast('App deployed successfully')
|
||||
@@ -315,7 +321,8 @@
|
||||
value: $app,
|
||||
path: newEditedPath || savedApp.path,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels
|
||||
})
|
||||
)
|
||||
) {
|
||||
@@ -366,7 +373,8 @@
|
||||
// it also means that customPath needs to be set to '' instead of undefined to unset it (when admin)
|
||||
custom_path:
|
||||
$userStore?.is_admin || $userStore?.is_super_admin ? (customPath ?? '') : undefined,
|
||||
preserve_on_behalf_of: preserveOnBehalfOf || undefined
|
||||
preserve_on_behalf_of: preserveOnBehalfOf || undefined,
|
||||
labels
|
||||
}
|
||||
})
|
||||
invalidateWorkspacePaths($workspaceStore!)
|
||||
@@ -375,7 +383,8 @@
|
||||
value: structuredClone($state.snapshot($app)),
|
||||
path: npath,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels: $state.snapshot(labels)
|
||||
}
|
||||
const appHistory = await AppService.getAppHistoryByPath({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -629,7 +638,8 @@
|
||||
value: $app,
|
||||
path: newEditedPath || savedApp.path,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -728,6 +738,7 @@
|
||||
})
|
||||
|
||||
let customPath = $state(savedApp?.custom_path)
|
||||
let labels = $state(untrack(() => initialLabels))
|
||||
|
||||
$effect(() => {
|
||||
if ($openDebugRun == undefined) {
|
||||
@@ -757,7 +768,8 @@
|
||||
value: $app,
|
||||
path: newEditedPath || savedApp?.path,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -801,7 +813,8 @@
|
||||
value: $app,
|
||||
path: newEditedPath || savedApp.path,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels
|
||||
},
|
||||
button: {
|
||||
text: 'Looks good, deploy',
|
||||
@@ -854,6 +867,7 @@
|
||||
bind:pathError
|
||||
bind:newEditedPath
|
||||
bind:preserveOnBehalfOf
|
||||
bind:labels
|
||||
hideSecretUrl={false}
|
||||
/>
|
||||
</DrawerContent>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import EEOnly from '$lib/components/EEOnly.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import LabelsInput from '$lib/components/LabelsInput.svelte'
|
||||
import OnBehalfOfSelector, {
|
||||
type OnBehalfOfChoice
|
||||
} from '$lib/components/OnBehalfOfSelector.svelte'
|
||||
@@ -38,6 +39,7 @@
|
||||
newPath,
|
||||
hideSecretUrl = false,
|
||||
preserveOnBehalfOf = $bindable(false),
|
||||
labels = $bindable(),
|
||||
rawApp = false,
|
||||
newApp = false
|
||||
}: {
|
||||
@@ -55,6 +57,7 @@
|
||||
newPath: string
|
||||
hideSecretUrl?: boolean
|
||||
preserveOnBehalfOf?: boolean
|
||||
labels?: string[] | undefined
|
||||
// Raw apps need cross-origin isolation (wm_coep) to be embeddable. Classic
|
||||
// (low-code) apps must NOT get the flag — it would force COEP on the
|
||||
// document and break no-CORP cross-origin subresources (external images,
|
||||
@@ -201,6 +204,8 @@
|
||||
bind:value={summary}
|
||||
/>
|
||||
</div>
|
||||
<div class="pt-3"></div>
|
||||
<LabelsInput bind:labels class="-mt-4" />
|
||||
<div class="py-6"></div>
|
||||
<label for="deploymentMsg" class="text-emphasis text-xs font-semibold">Deployment message</label>
|
||||
<div class="w-full pt-1">
|
||||
|
||||
@@ -144,6 +144,8 @@ export interface AppEditorProps {
|
||||
path: string
|
||||
policy: Policy
|
||||
summary: string
|
||||
/** Initial labels for the app, threaded from the loaded app data. */
|
||||
labels?: string[]
|
||||
/** Deployed app value the autosave `discardIf` compares against, so an
|
||||
* edit reverting to deployed clears the draft instead of leaving a no-op.
|
||||
* `undefined` for draft-only paths (no deployed baseline). */
|
||||
@@ -157,6 +159,7 @@ export interface AppEditorProps {
|
||||
summary: string
|
||||
policy: any
|
||||
custom_path?: string
|
||||
labels?: string[]
|
||||
}
|
||||
| undefined
|
||||
version?: number | undefined
|
||||
|
||||
@@ -71,6 +71,8 @@
|
||||
summary?: string
|
||||
path: string
|
||||
newPath?: string | undefined
|
||||
/** Initial labels for the app, threaded from the loaded app data. */
|
||||
labels?: string[]
|
||||
savedApp?:
|
||||
| {
|
||||
value: any
|
||||
@@ -82,6 +84,7 @@
|
||||
/** No deployed counterpart exists (draft-only); disables Diff. */
|
||||
no_deployed?: boolean
|
||||
custom_path?: string
|
||||
labels?: string[]
|
||||
}
|
||||
| undefined
|
||||
diffDrawer?: DiffDrawer | undefined
|
||||
@@ -140,6 +143,7 @@
|
||||
summary = $bindable(''),
|
||||
path,
|
||||
newPath = undefined,
|
||||
labels = undefined,
|
||||
savedApp = $bindable(undefined),
|
||||
diffDrawer = undefined,
|
||||
onNavigate,
|
||||
@@ -1612,6 +1616,7 @@
|
||||
{diffDrawer}
|
||||
{newApp}
|
||||
{newPath}
|
||||
{labels}
|
||||
appPath={path}
|
||||
{liveEditorDraftStoragePath}
|
||||
{autosaveWorkspace}
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
summary: string
|
||||
policy: any
|
||||
custom_path?: string
|
||||
labels?: string[]
|
||||
/** No deployed counterpart exists (draft-only); disables Diff. */
|
||||
no_deployed?: boolean
|
||||
}
|
||||
@@ -109,6 +110,8 @@
|
||||
version?: number | undefined
|
||||
newApp: boolean
|
||||
newPath?: string
|
||||
/** Initial labels for the app, threaded from the loaded app data. */
|
||||
labels?: string[]
|
||||
appPath: string
|
||||
runnables: Record<string, Runnable>
|
||||
files: Record<string, string> | undefined
|
||||
@@ -169,6 +172,7 @@
|
||||
version = $bindable(undefined),
|
||||
newApp,
|
||||
newPath = '',
|
||||
labels: initialLabels = undefined,
|
||||
appPath,
|
||||
runnables,
|
||||
data,
|
||||
@@ -331,7 +335,8 @@
|
||||
policy,
|
||||
deployment_message: deploymentMsg,
|
||||
custom_path: customPath,
|
||||
preserve_on_behalf_of: preserveOnBehalfOf || undefined
|
||||
preserve_on_behalf_of: preserveOnBehalfOf || undefined,
|
||||
labels
|
||||
},
|
||||
js,
|
||||
css
|
||||
@@ -345,7 +350,8 @@
|
||||
value: structuredClone(stateSnapshot(app)),
|
||||
path: path,
|
||||
policy: policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels: $state.snapshot(labels)
|
||||
}
|
||||
closeSaveDrawer()
|
||||
sendUserToast('App deployed successfully')
|
||||
@@ -454,7 +460,8 @@
|
||||
// custom_path requires admin so to accept update without it, we need to send as undefined when non-admin (when undefined, it will be ignored)
|
||||
// it also means that customPath needs to be set to '' instead of undefined to unset it (when admin)
|
||||
custom_path:
|
||||
$userStore?.is_admin || $userStore?.is_super_admin ? (customPath ?? '') : undefined
|
||||
$userStore?.is_admin || $userStore?.is_super_admin ? (customPath ?? '') : undefined,
|
||||
labels
|
||||
},
|
||||
js,
|
||||
css
|
||||
@@ -466,7 +473,8 @@
|
||||
value: structuredClone(stateSnapshot(app)),
|
||||
path: npath,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels: $state.snapshot(labels)
|
||||
}
|
||||
const appHistory = await AppService.getAppHistoryByPath({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -587,6 +595,7 @@
|
||||
|
||||
let customPath = $state(savedApp?.custom_path)
|
||||
let customPathError = $state('')
|
||||
let labels = $state(untrack(() => initialLabels))
|
||||
|
||||
let jobsDrawerOpen = $state(false)
|
||||
|
||||
@@ -600,7 +609,8 @@
|
||||
value: app,
|
||||
path: newEditedPath || savedApp?.path,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
custom_path: customPath,
|
||||
labels
|
||||
})
|
||||
)
|
||||
|
||||
@@ -690,6 +700,7 @@
|
||||
bind:pathError
|
||||
bind:newEditedPath
|
||||
bind:preserveOnBehalfOf
|
||||
bind:labels
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
summary: string
|
||||
policy: any
|
||||
custom_path?: string
|
||||
labels?: string[]
|
||||
}
|
||||
| undefined = $state(undefined)
|
||||
let redraw = $state(0)
|
||||
@@ -297,7 +298,8 @@
|
||||
value: backendApp_.value as App,
|
||||
path: backendApp_.path,
|
||||
policy: backendApp_.policy,
|
||||
custom_path: backendApp_.custom_path
|
||||
custom_path: backendApp_.custom_path,
|
||||
labels: backendApp_.labels
|
||||
}
|
||||
// "Load another user's draft" handoff: render their value. Overlay mode (we
|
||||
// have our own draft) hard-locks saves until the user confirms overwriting
|
||||
@@ -409,7 +411,8 @@
|
||||
value: app_.value as App,
|
||||
path: app_.path,
|
||||
policy: app_.policy,
|
||||
custom_path: app_.custom_path
|
||||
custom_path: app_.custom_path,
|
||||
labels: app_.labels
|
||||
}
|
||||
redraw++
|
||||
}
|
||||
@@ -478,6 +481,7 @@
|
||||
{onRestore}
|
||||
summary={app.summary}
|
||||
app={app.value}
|
||||
labels={app.labels}
|
||||
{deployedBaseline}
|
||||
newPath={app.value?.draft_path ?? app.path}
|
||||
path={page.params.path ?? ''}
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
// let lastVersion = 0
|
||||
let policy: any = $state({})
|
||||
let summary = $state('')
|
||||
let labels = $state<string[] | undefined>(undefined)
|
||||
/** User-typed path from `RawAppEditorHeader` when it differs from
|
||||
* `savedApp.path`; mirrored into the draft below as `draft_path` for the
|
||||
* home list's friendly name. */
|
||||
@@ -72,6 +73,7 @@
|
||||
summary: string
|
||||
policy: any
|
||||
custom_path?: string
|
||||
labels?: string[]
|
||||
no_deployed?: boolean
|
||||
}
|
||||
| undefined = $state(undefined)
|
||||
@@ -140,6 +142,7 @@
|
||||
if (extractedData) data = extractedData
|
||||
files = app.value.files
|
||||
summary = app.summary
|
||||
labels = app.labels
|
||||
// lastVersion = app.version
|
||||
policy = app.policy
|
||||
// Prefer the saved `draft_path` so the topbar shows the pending name, not
|
||||
@@ -179,6 +182,10 @@
|
||||
loadedFromDraft = false
|
||||
draftSavedAt = undefined
|
||||
deployedAt = undefined
|
||||
// `labels` is route-level state; reset it too so a fresh draft doesn't
|
||||
// inherit (and then deploy) the previously-opened app's labels. The
|
||||
// import branch re-seeds it via extractRawApp below.
|
||||
labels = undefined
|
||||
// Brand-new raw app: no deployed baseline, so never discard-on-equal.
|
||||
deployedBaseline = undefined
|
||||
// Suspend autosave across the bootstrap: the seed template and the
|
||||
@@ -355,6 +362,7 @@
|
||||
path: backendApp_.path,
|
||||
policy: backendApp_.policy,
|
||||
custom_path: backendApp_.custom_path,
|
||||
labels: backendApp_.labels,
|
||||
no_deployed: backendApp_.no_deployed
|
||||
}
|
||||
// Extract the effective raw app into the editor's local pieces. The bundle
|
||||
@@ -458,7 +466,8 @@
|
||||
value: structuredClone(stateSnapshot(prev.value)),
|
||||
path: prev.path,
|
||||
policy: structuredClone(stateSnapshot(policy)),
|
||||
custom_path: prev.custom_path
|
||||
custom_path: prev.custom_path,
|
||||
labels: prev.labels
|
||||
}
|
||||
redraw++
|
||||
}
|
||||
@@ -548,6 +557,7 @@
|
||||
bind:summary
|
||||
bind:pendingDraftPath
|
||||
{newPath}
|
||||
{labels}
|
||||
path={page.params.path ?? ''}
|
||||
liveEditorDraftStoragePath={path}
|
||||
{policy}
|
||||
|
||||
Reference in New Issue
Block a user