From da45e699c8aefeede172c90769ef4f4b182fec0c Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 28 Jun 2026 11:43:09 +0200 Subject: [PATCH] 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) * refactor(apps): drop redundant labels cast in app edit restore path Co-Authored-By: Claude Opus 4.8 (1M context) * 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) * 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) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../components/apps/editor/AppEditor.svelte | 2 ++ .../apps/editor/AppEditorHeader.svelte | 30 ++++++++++++++----- .../apps/editor/AppEditorHeaderDeploy.svelte | 5 ++++ frontend/src/lib/components/apps/types.ts | 3 ++ .../components/raw_apps/RawAppEditor.svelte | 5 ++++ .../raw_apps/RawAppEditorHeader.svelte | 21 +++++++++---- .../(logged)/apps/edit/[...path]/+page.svelte | 8 +++-- .../apps_raw/edit/[...path]/+page.svelte | 12 +++++++- 8 files changed, 70 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index f6e3245c2c..64cc94f551 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -69,6 +69,7 @@ path, policy, summary, + labels, deployedBaseline = undefined, fromHub = false, diffDrawer = undefined, @@ -886,6 +887,7 @@ 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} /> diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte index 0d5fc0c44a..01c0cb3028 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte @@ -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} /> +
+
diff --git a/frontend/src/lib/components/apps/types.ts b/frontend/src/lib/components/apps/types.ts index d8ac000ba5..960e273ac3 100644 --- a/frontend/src/lib/components/apps/types.ts +++ b/frontend/src/lib/components/apps/types.ts @@ -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 diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 18f9c1af0c..d93da9a46c 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -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} diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 9853fb69bd..2d7bed8333 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -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 files: Record | 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 /> diff --git a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte index ea87409171..7eb5d30106 100644 --- a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte @@ -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 ?? ''} diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte index 0cf374f0b1..6227776ec4 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte @@ -57,6 +57,7 @@ // let lastVersion = 0 let policy: any = $state({}) let summary = $state('') + let labels = $state(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}