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:
Ruben Fiszel
2026-06-28 11:43:09 +02:00
committed by GitHub
parent c479afab8e
commit da45e699c8
8 changed files with 70 additions and 16 deletions
@@ -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}