You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Software: Multi-version App Store apps for iOS, iPadOS, and Android - #54915
Adds the multi-version UI for App Store app titles (iOS/iPadOS/Android) on the software title page. Each version carries its own name, managed app configuration, label scope, self-service, and (iOS/iPadOS) auto-update window, so admins can give different host groups different app settings on the same fleet.
Library section (multi-version iOS/iPadOS/Android App Store titles only; macOS VPP and in-house .ipa keep today's single-row UI):
Grey app-metadata header above the accordion list: store icon + bold store name + version with "Updated every hour." tooltip (iOS) or AndroidLatestVersionWithTooltip (Android).
Per-version accordion rows with the admin's version name, file-configuration-profile icon, per-version status counts, self-service / auto-update / labels icons, expand → pencil + trash in the panel.
New Add version button on the description row (disabled at 10 versions, disabled in GitOps with the standard tooltip).
Modals:
Add version — Name (required, unique), Configuration (Editor; XML scaffold for iOS/iPadOS, {} for Android), Self-service (slider, iOS/iPadOS), Category + "View end user experience" link (iOS/iPadOS when self-service on), Auto updates with window times (iOS/iPadOS), Target with first-added-wins callout. Target defaults to Custom.
Edit version — same form, pre-filled. Three-way configuration save: unchanged scaffold = no change (field omitted), empty editor = clear (null for iOS/iPadOS, {} for Android to match the BE's clear values), content = set. IEditAppStoreAppFormData.configuration widened to accept null.
Delete version — title switches to "Delete version"; the "Custom icon and display name will be deleted." sentence appears on last-version delete.
Info card (SoftwareSummaryCard):
"Versions" count = admin-added versions on multi-version titles.
Actions dropdown collapses to a single pencil Edit (appearance) button (secondary variant).
Self-service and Auto-update chips suppressed (now per-version).
Shared-component refactors (used by this PR + VPP macOS edit):
CategoriesSelector extracted and exported from SoftwareOptionsSelector; new optional label and previewButtonLabel props so the version modal can use "Category" + "View end user experience" without changing the fleet-level copy.
DropdownTargetLabelSelector gained labelInfoBanner and error props so the version modal plugs the first-added-wins banner through the component and surfaces the labelTargets validation error, eliminating the duplicate "Target" wrapper.
Form validation fixes:
Shared getAutoUpdateWindowDurationMinutes helper (in pages/SoftwarePage/helpers) wrap-aware so overnight windows under one hour (e.g. 23:30-00:15) no longer bypass the ≥60 min check. Applied to both the version-form validator and EditAutoUpdateConfigModal/helpers.tsx.
Android configuration arrives as a parsed object from axios; versionToFormData stringifies before handing to the Editor. IAppStoreApp.configuration and IAppStoreAppVersion.configuration widened to string | Record<string, unknown>.
Backend contract: depends on #53320 (API + YAML) + #54479 (migration) + #54590 (endpoints) + #54908 (install precedence) + #54805 (GitOps). This PR targets the feat/47904-multiple-app-instances feature branch, not main; once that feature branch merges to main the API shape will match my FE types.
Follow-up PR:#54954 ships host detail configuration icon + version-name tooltip, activity feed per-version copy + edited-software details modal Version row + readonly configuration editor, iOS/iPadOS install details sheet config-update sentence, and Setup experience helper text per platform tab.
Known gap not in either PR: per-version /hosts filter wiring. BE has not added a VPPAppVersionID / AppStoreAppVersionID filter to HostListOptions (confirmed on 53639-app-store-app-version-precedence and 53640-app-store-apps-gitops branches). Current click-through on per-version status counts routes to the title-wide host filter; needs a new BE ticket to specify the query key.
Testing
Added automated tests across the touched suites (modal, cell, helper, and form coverage)
QA'd all new/changed functionality manually
Manual steps mirror the parent story's core flow:
Open an iOS/iPadOS or Android App Store title in Library.
Click Add version → add Production and Test versions with different configurations and label scopes.
Verify two version rows render with their own counts + icons, grey header above shows the app metadata.
Edit a version → change name, labels, auto-update window → save.
Edit a version → delete the stored configuration bytes → confirm BE records the clear (null for iOS/iPadOS, {} for Android).
Try an overnight auto-update window shorter than 60 minutes (e.g. 23:30 → 00:15) → confirm inline error, not a 400 from the backend.
Delete the first-added version → confirm 2-sentence modal.
Delete the last version → confirm the "Custom icon and display name will be deleted." sentence appears.
Verify macOS VPP and in-house .ipa titles still render the single-row Library UI.
AI
AI: Claude Code (claude-opus-4-7)
Screenrecording
Note: Save buttons are disabled for editing because app_store_apps is null in my branch so it's hitting a FE validation for a bad id
❌ Patch coverage is 57.41325% with 135 lines in your changes missing coverage. Please review. ⚠️ Please upload report for BASE (feat/47904-multiple-app-instances@08aa50e). Learn more about missing BASE report.
Adds the versions-oriented UI on the software title page for iOS, iPadOS,
and Android App Store app titles. Each version carries its own name,
managed app configuration, label scope, self-service, and (iOS/iPadOS)
auto-update window, so admins can give different host groups different
settings without creating a separate fleet.
- Library section renders a grey app-metadata header (store icon + name +
version with tooltip) above an accordion of per-version rows.
- Per-version rows show status counts, self-service / auto-update /
labels icons on the header, expand to pencil + trash in the panel.
- New Add version button (disabled at 10 versions and in GitOps mode)
opens a modal with Name, Configuration (XML/JSON Editor), Self-service,
Category + preview end-user experience, Auto updates window, and
Target (defaults to Custom on 2nd+ version with the first-added-wins
callout).
- Edit version modal reuses the same form, pre-filled from the version.
- Delete version modal switches to "Delete version" title and surfaces
the "Custom icon and display name will be deleted." sentence on the
last-version delete.
- Info card: Versions count shows admin-added versions; Actions dropdown
collapses to a single pencil Edit (appearance) button; Self-service
and Auto-update chips suppressed (per-version now).
- macOS VPP and in-house .ipa titles keep the existing single-row UI.
- Migrate AddVersionModal and EditVersionModal to the shared
`useFormValidation` hook (reference pattern per `UserForm`/`ApiUserForm`).
Server-side name errors now render inline AND fire a toast.
- Wrap Target, Category, and Auto updates sections in `FormField`; drop
the hand-rolled label styles.
- Use `vertical-form-layout`, `vertical-modal-layout`, and
`flex-column-8px-gap` mixins instead of inline flex-column rules.
- Drop the dead `softwareTitleId` prop + hidden input from AddVersionModal
(the add route targets the title via `app_store_id` + `platform`).
- `<b>` → `<strong>` in new copy.
- Hardcode `variant="secondary"` + `size="small"` on the single Edit
(appearance) button; drop the `singleEditButtonVariant` prop.
- Rename `showCustomIconSentence` → `isDeletingCustomMetaData`.
- Trim verbose comments on config scaffolds and
`MAX_APP_STORE_APP_VERSIONS_PER_TITLE`.
- Hoist `getAutoUpdateTooltip` to `pages/SoftwarePage/helpers.tsx` so
SoftwareNameCell, SoftwareSummaryCard chip, and LibraryItemAccordion
row icon all share the single copy that matches Figma.
- Row-level icon buttons use `size="small"` which renders subdued
icon-only at 28x28 natively; dropped the scoped SCSS override.
- Drop the redundant Android platform check on the preview-end-user
handler and modal — the button is already iOS/iPadOS-only inside
VersionFormFields.
- Drop the Fleet Desktop tail from the Edit version toast — Fleet
Desktop isn't the install surface for iOS/iPadOS or Android.
- Strip issue refs from comments; tighten remaining comments.
- Add shared `validateVersionForm` helper covering name (required + unique),
Custom target (at least one label), auto-update window (both times
required, HH:MM format, >=60min or wrap-to-next-day), and Android JSON
validity. Wire error display on Target FormField + window inputs.
- EditVersionModal: new `titleDisplayName` prop so Auto updates help text
shows the app's display name instead of falling back to the admin's
version label (e.g. "Production").
- Parse Android configuration as JSON before sending — backend strict-
decodes `configuration` as an object, not a string.
- GitOps lock: wrap EditVersionModal Save button in
`GitOpsModeTooltipWrapper` so GitOps-managed versions can't be PATCHed
from the UI (parity with Add + Delete).
- Prettier fix on AgentOptionsPage.tests.tsx (unrelated — surfaced in CI).
- EditVersionModal: match Add on configuration (undefined = no change,
not silent clear); block Save on shim-id (-1 sentinel so it can't
collide with a real version id).
- Widen service `configuration` to string | Record<string, unknown> so
Android's parsed JSON isn't double-cast at the call site.
- Hoist EMPTY_XML / EMPTY_JSON scaffolds + `getEmptyConfigScaffold` into
VersionFormFields; drop the dupes from both modals.
- getAutoUpdateTooltip returns null when the window is incomplete.
- Coerce isSelfService / isAutoUpdateEnabled to bool at the row prop
boundary.
- SoftwareSummaryCard: lift versionsCount above the early return so the
installer-less branch reuses it.
- Trim stray blank line in LibraryItemAccordion scss.
- Add createMockAppStoreAppVersion factory; swap in-test literals.
- LibraryItemAccordion: smoke tests for the versions-layout row (name
renders without store version chip, self-service + auto-update icons,
auto-update icon hidden when off).
VersionFormFields:
- Swap the hand-rolled category block for the shared CategoriesSelector
(exported from SoftwareOptionsSelector); pass label="Category" and
previewButtonLabel="View end user experience" to override the shared
defaults for the version modal.
- Drop the outer FormField name="target" wrapper that was rendering
"Target" twice (once from the wrapper, once from the selector's own
title). Plumb the InfoBanner and labelTargets error through new
selector props instead.
CategoriesSelector (shared):
- Export the component (was module-private).
- Wrap in FormField so the label + preview button inherit the standard
form-field envelope.
- Preview button always inline-right of the label (subdued/small),
dropping the bottom-of-list bordered variant and its scss.
- New optional label / previewButtonLabel props for per-callsite copy.
DropdownTargetLabelSelector:
- New optional labelInfoBanner slot (renders between title and radios)
and error prop (replaces the title text + applies --error class).
Both default off, so other callers are unchanged.
LibraryItemAccordion row icons:
- Drop the textBalanced={false} opt-out so the Play Store / self-service
tooltips get the TooltipWrapper balanced default.
These auto-update values are silently ignored on add: the current POST request contract in server/service/vpp.go:54-66 has no auto-update fields and therefore never maps them into VPPAppTeam. Either extend the backend POST contract or PATCH the returned version_id; otherwise the new row is created with auto updates off despite the modal selection.
Use the correct accessible name for version editing
Enabling this package-specific self-service renderer for version rows gives its button the accessible name “Edit package,” even though it opens Edit version (the new test currently codifies that mismatch). Use “Edit version” when canActivateMultipleVersions is true so screen-reader users hear the correct target.
The fallback row uses SHIM_VERSION_ID = -1, but this callback still opens a delete flow that sends version_id=-1; the file-level invariant explicitly says version mutations must guard id >= 0. Hide the trash action for shim rows so the user cannot submit a guaranteed-invalid delete.
- EditVersionModal: three-way configurationPayload. Unchanged scaffold =
no change (undefined); empty editor = clear (null for iOS/iPadOS, {}
for Android, matching the backend's clear values); content = set.
Widen IEditAppStoreAppFormData.configuration and the inline
editAppStoreAppVersion param type to accept null.
- Fix overnight auto-update windows shorter than one hour (e.g.
23:30-00:15) that bypassed the >=60 min check. Extract
getAutoUpdateWindowDurationMinutes to SoftwarePage/helpers so
VersionFormFields and EditAutoUpdateConfigModal share one wrap-aware
implementation. New jest covers same-day, overnight, same-time,
the 45-min regression case, and malformed inputs.
- versionToFormData stringifies Android configurations before handing
to the Editor (BE sends json.RawMessage parsed to an object). Widen
IAppStoreApp.configuration and IAppStoreAppVersion.configuration to
string | Record<string, unknown>. EditConfigurationModal.getInitialValue
casts the apple branch to narrow.
The reason will be displayed to describe this comment to others. Learn more.
I deleted one of two versions on a title that isn't installed anywhere yet. It sent me back to the Software library even though the other version was still there. The Conditions say the row should disappear, so should a version delete stay on the page and refetch?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue: Resolves #53641
Description
Adds the multi-version UI for App Store app titles (iOS/iPadOS/Android) on the software title page. Each version carries its own name, managed app configuration, label scope, self-service, and (iOS/iPadOS) auto-update window, so admins can give different host groups different app settings on the same fleet.
Library section (multi-version iOS/iPadOS/Android App Store titles only; macOS VPP and in-house
.ipakeep today's single-row UI):AndroidLatestVersionWithTooltip(Android).file-configuration-profileicon, per-version status counts, self-service / auto-update / labels icons, expand → pencil + trash in the panel.Modals:
{}for Android), Self-service (slider, iOS/iPadOS), Category + "View end user experience" link (iOS/iPadOS when self-service on), Auto updates with window times (iOS/iPadOS), Target with first-added-wins callout. Target defaults to Custom.{}for Android to match the BE's clear values), content = set.IEditAppStoreAppFormData.configurationwidened to acceptnull.Info card (
SoftwareSummaryCard):Shared-component refactors (used by this PR + VPP macOS edit):
CategoriesSelectorextracted and exported fromSoftwareOptionsSelector; new optionallabelandpreviewButtonLabelprops so the version modal can use "Category" + "View end user experience" without changing the fleet-level copy.DropdownTargetLabelSelectorgainedlabelInfoBanneranderrorprops so the version modal plugs the first-added-wins banner through the component and surfaces the labelTargets validation error, eliminating the duplicate "Target" wrapper.Form validation fixes:
getAutoUpdateWindowDurationMinuteshelper (inpages/SoftwarePage/helpers) wrap-aware so overnight windows under one hour (e.g. 23:30-00:15) no longer bypass the ≥60 min check. Applied to both the version-form validator andEditAutoUpdateConfigModal/helpers.tsx.configurationarrives as a parsed object from axios;versionToFormDatastringifies before handing to the Editor.IAppStoreApp.configurationandIAppStoreAppVersion.configurationwidened tostring | Record<string, unknown>.Backend contract: depends on #53320 (API + YAML) + #54479 (migration) + #54590 (endpoints) + #54908 (install precedence) + #54805 (GitOps). This PR targets the
feat/47904-multiple-app-instancesfeature branch, notmain; once that feature branch merges to main the API shape will match my FE types.Follow-up PR: #54954 ships host detail configuration icon + version-name tooltip, activity feed per-version copy + edited-software details modal Version row + readonly configuration editor, iOS/iPadOS install details sheet config-update sentence, and Setup experience helper text per platform tab.
Known gap not in either PR: per-version
/hostsfilter wiring. BE has not added aVPPAppVersionID/AppStoreAppVersionIDfilter toHostListOptions(confirmed on53639-app-store-app-version-precedenceand53640-app-store-apps-gitopsbranches). Current click-through on per-version status counts routes to the title-wide host filter; needs a new BE ticket to specify the query key.Testing
Manual steps mirror the parent story's core flow:
{}for Android)..ipatitles still render the single-row Library UI.AI
AI: Claude Code (claude-opus-4-7)
Screenrecording
Note: Save buttons are disabled for editing because app_store_apps is null in my branch so it's hitting a FE validation for a bad id
Screen.Recording.2026-10-06.at.11.33.25.PM.mov
Screen.Recording.2026-10-06.at.11.32.35.PM.mov
GitOps mode disables saving edits
Screen.Recording.2026-10-06.at.11.40.15.PM.mov