diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTaskGroup.cs index 8a6dacd1e..e91a53569 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTaskGroup.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTaskGroup.cs @@ -29,10 +29,18 @@ public interface ISetupTaskGroup public ReviewTabViewModelBase GetReviewTabViewModel(); /// - /// Gets all the individual setup tasks that make up this group + /// Gets all the setup tasks that make up this group /// public IEnumerable SetupTasks { get; } + + /// + /// Gets all the DSC tasks that make up this group + /// + public IEnumerable DSCTasks + { + get; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/WinGetDscSettings.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/WinGetDscSettings.cs index cd30b5508..8086b0b5b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/WinGetDscSettings.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/WinGetDscSettings.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using YamlDotNet.Core; +using YamlDotNet.Serialization; + namespace DevHome.SetupFlow.Models.WingetConfigure; /// @@ -9,6 +12,7 @@ namespace DevHome.SetupFlow.Models.WingetConfigure; /// public class WinGetDscSettings : WinGetConfigSettingsBase { + [YamlMember(ScalarStyle = ScalarStyle.DoubleQuoted)] public string Id { get; set; } public string Source { get; set; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs index 0faf1612b..21c498c6d 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs @@ -8,6 +8,7 @@ using DevHome.SetupFlow.Models; using DevHome.SetupFlow.Models.WingetConfigure; using DevHome.SetupFlow.TaskGroups; +using Serilog; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -21,13 +22,8 @@ public enum ConfigurationFileKind public class ConfigurationFileBuilder { - private readonly SetupFlowOrchestrator _orchestrator; - - public ConfigurationFileBuilder(SetupFlowOrchestrator orchestrator) - { - _orchestrator = orchestrator; - } - + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ConfigurationFileBuilder)); + /// /// Builds an object that represents a config file that can be used by WinGet Configure to install /// apps and clone repositories.This is already formatted as valid yaml and can be written @@ -36,21 +32,33 @@ public ConfigurationFileBuilder(SetupFlowOrchestrator orchestrator) /// The config file object representing the yaml file. public WinGetConfigFile BuildConfigFileObjectFromTaskGroups(IList taskGroups, ConfigurationFileKind configurationFileKind) { - var listOfResources = new List(); - + List repoResources = []; + List appResources = []; foreach (var taskGroup in taskGroups) { if (taskGroup is RepoConfigTaskGroup repoConfigGroup) { // Add the GitDSC resource blocks to yaml - listOfResources.AddRange(GetResourcesForCloneTaskGroup(repoConfigGroup, configurationFileKind)); + repoResources.AddRange(GetResourcesForCloneTaskGroup(repoConfigGroup, configurationFileKind)); } else if (taskGroup is AppManagementTaskGroup appManagementGroup) { // Add the WinGetDsc resource blocks to yaml - listOfResources.AddRange(GetResourcesForAppManagementTaskGroup(appManagementGroup, configurationFileKind)); + appResources.AddRange(GetResourcesForAppManagementTaskGroup(appManagementGroup, configurationFileKind)); } } + + // If Git is not added to the apps to install and there are + // repositories to clone, add Git as a pre-requisite + var isGitAdded = appResources + .Select(r => r.Settings as WinGetDscSettings) + .Any(s => s.Id == DscHelpers.GitWinGetPackageId); + if (!isGitAdded && repoResources.Count > 0) + { + appResources.Add(CreateWinGetInstallForGitPreReq()); + } + + List listOfResources = [..appResources, ..repoResources]; if (listOfResources.Count == 0) { @@ -114,22 +122,24 @@ public string SerializeWingetFileObjectToString(WinGetConfigFile configFile) private List GetResourcesForCloneTaskGroup(RepoConfigTaskGroup repoConfigGroup, ConfigurationFileKind configurationFileKind) { var listOfResources = new List(); - var repoConfigTasks = repoConfigGroup.SetupTasks + var repoConfigTasks = repoConfigGroup.DSCTasks .Where(task => task is CloneRepoTask) .Select(task => task as CloneRepoTask) .ToList(); - if (repoConfigTasks.Count != 0) - { - listOfResources.Add(CreateWinGetInstallForGitPreReq()); - } - foreach (var repoConfigTask in repoConfigTasks) { - if (repoConfigTask.RepositoryToClone is GenericRepository genericRepository) - { - listOfResources.Add(CreateResourceFromTaskForGitDsc(repoConfigTask, genericRepository.RepoUri, configurationFileKind)); - } + try + { + if (!repoConfigTask.RepositoryToClone.IsPrivate) + { + listOfResources.Add(CreateResourceFromTaskForGitDsc(repoConfigTask, repoConfigTask.RepositoryToClone.RepoUri, configurationFileKind)); + } + } + catch (Exception e) + { + _log.Error($"Error creating a repository resource entry", e); + } } return listOfResources; @@ -143,7 +153,7 @@ private List GetResourcesForCloneTaskGroup(RepoConfigTaskG private List GetResourcesForAppManagementTaskGroup(AppManagementTaskGroup appManagementGroup, ConfigurationFileKind configurationFileKind) { var listOfResources = new List(); - var installList = appManagementGroup.SetupTasks + var installList = appManagementGroup.DSCTasks .Where(task => task is InstallPackageTask) .Select(task => task as InstallPackageTask) .ToList(); @@ -177,8 +187,16 @@ private WinGetConfigResource CreateResourceFromTaskForWinGetDsc(InstallPackageTa { Resource = DscHelpers.WinGetDscResource, Id = id, - Directives = new() { AllowPrerelease = true, Description = $"Installing {arguments.PackageId}" }, - Settings = new WinGetDscSettings() { Id = arguments.PackageId, Source = DscHelpers.DscSourceNameForWinGet }, + Directives = new() + { + AllowPrerelease = true, + Description = $"Installing {arguments.PackageId}", + }, + Settings = new WinGetDscSettings() + { + Id = arguments.PackageId, + Source = arguments.CatalogName, + }, }; } @@ -190,16 +208,13 @@ private WinGetConfigResource CreateResourceFromTaskForWinGetDsc(InstallPackageTa /// The WinGetConfigResource object that represents the block of yaml needed by GitDsc to clone the repository. private WinGetConfigResource CreateResourceFromTaskForGitDsc(CloneRepoTask task, Uri webAddress, ConfigurationFileKind configurationFileKind) { - // For normal cases, the Id will be null. This can be changed in the future when a use case for this Dsc File builder is needed outside the setup - // setup target flow. We can likely drop the if statement and just use whats in its body. - string id = null; + // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. + // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. + var id = $"Clone {task.RepositoryName}: {task.CloneLocation.FullName}"; var gitDependsOnId = DscHelpers.GitWinGetPackageId; if (configurationFileKind == ConfigurationFileKind.SetupTarget) { - // WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI. - // So we add a description to the Id to make it more readable in the UI. These do not need to be localized. - id = $"Clone {task.RepositoryName}" + ": " + task.CloneLocation.FullName; gitDependsOnId = $"{DscHelpers.GitWinGetPackageId} | Install: {DscHelpers.GitName}"; } @@ -223,7 +238,7 @@ private WinGetConfigResource CreateWinGetInstallForGitPreReq() return new WinGetConfigResource() { Resource = DscHelpers.WinGetDscResource, - Id = $"{DscHelpers.GitWinGetPackageId} | Install: {DscHelpers.GitName}", + Id = DscHelpers.GitWinGetPackageId, Directives = new() { AllowPrerelease = true, Description = $"Installing {DscHelpers.GitName}" }, Settings = new WinGetDscSettings() { Id = DscHelpers.GitWinGetPackageId, Source = DscHelpers.DscSourceNameForWinGet }, }; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs index bb2193809..e4d6f44eb 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs @@ -73,7 +73,7 @@ private sealed class PackageCache /// /// Occurs when a package selection has changed /// - public event EventHandler PackageSelectionChanged; + public event EventHandler SelectedPackagesItemChanged; public PackageProvider(PackageViewModelFactory packageViewModelFactory) { @@ -107,7 +107,7 @@ public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanentl _log.Debug($"Creating view model for package [{package.Id}]"); var viewModel = _packageViewModelFactory(package); viewModel.SelectionChanged += OnPackageSelectionChanged; - viewModel.SelectionChanged += (sender, package) => PackageSelectionChanged?.Invoke(sender, package); + viewModel.VersionChanged += OnSelectedPackageVersionChanged; // Cache if requested if (cachePermanently) @@ -122,10 +122,25 @@ public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanentl return viewModel; } + } + + private void OnSelectedPackageVersionChanged(object sender, string version) + { + var packageViewModel = sender as PackageViewModel; + if (packageViewModel?.IsSelected == true) + { + // Notify subscribers that an item in the list of selected packages has changed + SelectedPackagesItemChanged?.Invoke(packageViewModel, EventArgs.Empty); + } } - public void OnPackageSelectionChanged(object sender, PackageViewModel packageViewModel) - { + private void OnPackageSelectionChanged(object sender, bool isSelected) + { + if (sender is not PackageViewModel packageViewModel) + { + return; + } + lock (_lock) { if (packageViewModel.IsSelected) @@ -154,12 +169,17 @@ public void OnPackageSelectionChanged(object sender, PackageViewModel packageVie { _log.Debug($"Removing package [{packageViewModel.Package.Id}] from cache"); _packageViewModelCache.Remove(packageViewModel.UniqueKey); + packageViewModel.SelectionChanged -= OnPackageSelectionChanged; + packageViewModel.VersionChanged -= OnSelectedPackageVersionChanged; } // Remove from the selected package collection _selectedPackages.Remove(packageViewModel); } } + + // Notify subscribers that an item in the list of selected packages has changed + SelectedPackagesItemChanged?.Invoke(packageViewModel, EventArgs.Empty); } /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index 912253ccc..a1d3cb4f0 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -54,6 +54,7 @@ public static class StringResourceKey public static readonly string EditClonePathDialog = nameof(EditClonePathDialog); public static readonly string EditClonePathDialogUncheckCheckMark = nameof(EditClonePathDialogUncheckCheckMark); public static readonly string FilePickerFileTypeOption = nameof(FilePickerFileTypeOption); + public static readonly string FilePickerSingleFileTypeOption = nameof(FilePickerSingleFileTypeOption); public static readonly string FileTypeNotSupported = nameof(FileTypeNotSupported); public static readonly string InstalledPackage = nameof(InstalledPackage); public static readonly string InstalledPackageReboot = nameof(InstalledPackageReboot); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 5dddf68b4..2f00a48b7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -323,7 +323,11 @@ {0} files - Dropdown option for a file picker. {0} is replaced by a file type (e.g. JSON, YAML, etc ...) + {Locked="{0}"}Dropdown option for a file picker. {0} is replaced by a file type (e.g. JSON, YAML, etc ...) + + + {0} file + {Locked="{0}"}Dropdown option for a file picker. {0} is replaced by a file type (e.g. JSON, YAML, etc ...) File type not supported @@ -522,7 +526,7 @@ Header text for a group of controls giving multiple choices for configuring the machine, but not a full setup flow - Clone repositories and install applications at once + Clone repositories, install applications, and generate Winget Configuration files together Body text description for a card than when clicked takes the user to a multi-step flow for setting up their machine @@ -561,6 +565,10 @@ Remove all Label for removing all items from selection + + Applications that have been previously installed cannot be installed again. They will be included in your generated configuration files. + Message displayed when a user selects an application that is already installed + Remove Text announced when screen readers focus on the 'Remove' button. The 'Remove' button allows users to remove an application from their cart @@ -633,6 +641,14 @@ Restore Label for restore button + + Generate Configuration file + Text for a generating configuration file button + + + Generate a WinGet Configuration file (.winget) to repeat this set up in the future or share it with others. + {Locked="WinGet",".winget"}Tooltip text about the generated configuration file + Set up details Header for a section detailing the set up steps to be performed. "Set up" is the noun diff --git a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml index 0a50176ff..d8072694f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Styles/AppManagement_ThemeResources.xaml @@ -40,7 +40,6 @@ x:Uid="ms-resource:///DevHome.SetupFlow/Resources/Installed"/> - @@ -50,7 +49,6 @@ - @@ -93,7 +91,6 @@ SelectedItem="{Binding SelectedVersion, Mode=TwoWay}" ItemsSource="{Binding AvailableVersions}" /> public IEnumerable SetupTasks => CloneTasks; + public IEnumerable DSCTasks => SetupTasks; + /// /// Gets all tasks that need to be ran. /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs index df87d8420..4bd1bf848 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SetupTargetTaskGroup.cs @@ -45,6 +45,8 @@ public SetupTargetTaskGroup( public IEnumerable SetupTasks => new List() { _setupTargetTaskGroup }; + public IEnumerable DSCTasks => SetupTasks; + public SetupPageViewModelBase GetSetupPageViewModel() => _setupTargetViewModel; public ReviewTabViewModelBase GetReviewTabViewModel() => _setupTargetReviewViewModel; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AppManagementViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AppManagementViewModel.cs index d9bee99c1..c19498b6b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AppManagementViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AppManagementViewModel.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading; @@ -8,7 +9,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; -using DevHome.Common.Services; using DevHome.SetupFlow.Services; using Microsoft.Extensions.Hosting; using Serilog; @@ -21,9 +21,7 @@ public partial class AppManagementViewModel : SetupPageViewModelBase private readonly ShimmerSearchViewModel _shimmerSearchViewModel; private readonly SearchViewModel _searchViewModel; private readonly PackageCatalogListViewModel _packageCatalogListViewModel; - private readonly IWindowsPackageManager _wpm; private readonly PackageProvider _packageProvider; - private readonly IScreenReaderService _screenReaderService; /// /// Current view to display in the main content control @@ -31,6 +29,9 @@ public partial class AppManagementViewModel : SetupPageViewModelBase [ObservableProperty] private ObservableObject _currentView; + [ObservableProperty] + private bool _showInstalledPackageWarning; + public ReadOnlyObservableCollection SelectedPackages => _packageProvider.SelectedPackages; public string ApplicationsAddedText => SelectedPackages.Count == 1 ? @@ -43,19 +44,13 @@ public AppManagementViewModel( ISetupFlowStringResource stringResource, SetupFlowOrchestrator orchestrator, IHost host, - IWindowsPackageManager wpm, PackageProvider packageProvider) : base(stringResource, orchestrator) { - _wpm = wpm; _packageProvider = packageProvider; _searchViewModel = host.GetService(); _shimmerSearchViewModel = host.GetService(); _packageCatalogListViewModel = host.GetService(); - _screenReaderService = host.GetService(); - - _packageProvider.PackageSelectionChanged += (_, _) => OnPropertyChanged(nameof(ApplicationsAddedText)); - _packageProvider.PackageSelectionChanged += (_, _) => OnPropertyChanged(nameof(EnableRemoveAll)); PageTitle = StringResource.GetLocalized(StringResourceKey.ApplicationsPageTitle); @@ -110,4 +105,26 @@ private void RemoveAllPackages() package.IsSelected = false; } } + + [RelayCommand] + private void OnLoaded() + { + _packageProvider.SelectedPackagesItemChanged += OnPackageSelectionChanged; + } + + [RelayCommand] + private void OnUnloaded() + { + _packageProvider.SelectedPackagesItemChanged -= OnPackageSelectionChanged; + } + + private void OnPackageSelectionChanged(object sender, EventArgs args) + { + // Notify UI to update + OnPropertyChanged(nameof(ApplicationsAddedText)); + OnPropertyChanged(nameof(EnableRemoveAll)); + + // Show warning if any selected package is installed + ShowInstalledPackageWarning = SelectedPackages.Any(p => !p.CanInstall); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs index b5159f4f5..6008195d6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageViewModel.cs @@ -3,16 +3,13 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using DevHome.Common.Extensions; using DevHome.Common.Services; using DevHome.Contracts.Services; using DevHome.SetupFlow.Models; using DevHome.SetupFlow.Services; -using Microsoft.Extensions.Hosting; using Microsoft.Internal.Windows.DevHome.Helpers.Restore; using Microsoft.UI.Xaml.Media.Imaging; using Windows.Storage.Streams; @@ -39,19 +36,23 @@ public partial class PackageViewModel : ObservableObject private readonly Lazy _packageDarkThemeIcon; private readonly Lazy _packageLightThemeIcon; - private readonly Lazy _installPackageTask; private readonly ISetupFlowStringResource _stringResource; private readonly IWinGetPackage _package; private readonly IWindowsPackageManager _wpm; private readonly IThemeSelectorService _themeSelector; - private readonly IScreenReaderService _screenReaderService; - private readonly SetupFlowOrchestrator _setupFlowOrchestrator; + private readonly IScreenReaderService _screenReaderService; + private readonly SetupFlowOrchestrator _orchestrator; /// /// Occurs after the package selection changes /// - public event EventHandler SelectionChanged; + public event EventHandler SelectionChanged; + + /// + /// Occurs after the package version has changed + /// + public event EventHandler VersionChanged; /// /// Indicates if a package is selected @@ -63,11 +64,8 @@ public partial class PackageViewModel : ObservableObject [ObservableProperty] [NotifyPropertyChangedFor(nameof(TooltipVersion))] [NotifyPropertyChangedFor(nameof(PackageFullDescription))] - [NotifyPropertyChangedFor(nameof(CanSelect))] private string _selectedVersion; - public bool CanSelect => IsSelectable(); - public bool ShowVersionList => IsVersioningSupported(); public PackageViewModel( @@ -76,22 +74,21 @@ public PackageViewModel( IWinGetPackage package, IThemeSelectorService themeSelector, IScreenReaderService screenReaderService, - IHost host, SetupFlowOrchestrator orchestrator) { _stringResource = stringResource; _wpm = wpm; _package = package; _themeSelector = themeSelector; - _screenReaderService = screenReaderService; - _setupFlowOrchestrator = orchestrator; + _screenReaderService = screenReaderService; + _orchestrator = orchestrator; // Lazy-initialize optional or expensive view model members _packageDarkThemeIcon = new Lazy(() => GetIconByTheme(RestoreApplicationIconTheme.Dark)); _packageLightThemeIcon = new Lazy(() => GetIconByTheme(RestoreApplicationIconTheme.Light)); - _installPackageTask = new Lazy(() => CreateInstallTask(host.GetService().ActivityId)); SelectedVersion = GetDefaultSelectedVersion(); + InstallPackageTask = CreateInstallTask(); } public PackageUniqueKey UniqueKey => _package.UniqueKey; @@ -106,8 +103,7 @@ public PackageViewModel( public IReadOnlyList AvailableVersions => _package.AvailableVersions; - // When in setup target flow don't disable installed packaged. - public bool IsInstalled => _setupFlowOrchestrator.IsSettingUpATargetMachine ? false : _package.IsInstalled; + public bool IsInstalled => _package.IsInstalled; public string CatalogName => _package.CatalogName; @@ -130,12 +126,14 @@ public PackageViewModel( public string TooltipSource => _stringResource.GetLocalized(StringResourceKey.PackageSourceTooltip, CatalogName); public string TooltipPublisher => _stringResource.GetLocalized(StringResourceKey.PackagePublisherNameTooltip, PublisherName); + + public bool CanInstall => _orchestrator.IsSettingUpATargetMachine || !IsInstalled || _package.InstalledVersion != SelectedVersion; public string ButtonAutomationName => IsSelected ? _stringResource.GetLocalized(StringResourceKey.RemoveApplication) : _stringResource.GetLocalized(StringResourceKey.AddApplication); - public InstallPackageTask InstallPackageTask => _installPackageTask.Value; + public InstallPackageTask InstallPackageTask { get; private set; } /// /// Gets the URI for the "Learn more" button @@ -171,16 +169,14 @@ public Uri GetLearnMoreUri() return new Uri("https://github.com/microsoft/winget-pkgs"); } - partial void OnIsSelectedChanged(bool value) => SelectionChanged?.Invoke(null, this); - + partial void OnIsSelectedChanged(bool value) => SelectionChanged?.Invoke(this, value); + partial void OnSelectedVersionChanged(string value) { - // If the selected version changed to a version that cannot be selected - // (e.g. installed version) then unselect the package - if (IsSelected && !IsSelectable()) - { - IsSelected = false; - } + // Update the install task with the new selected version + InstallPackageTask = CreateInstallTask(); + + VersionChanged?.Invoke(this, SelectedVersion); } /// @@ -225,9 +221,9 @@ private BitmapImage CreateBitmapImage(IRandomAccessStream stream) return bitmapImage; } - private InstallPackageTask CreateInstallTask(Guid activityId) + private InstallPackageTask CreateInstallTask() { - return _package.CreateInstallTask(_wpm, _stringResource, SelectedVersion, activityId); + return _package.CreateInstallTask(_wpm, _stringResource, SelectedVersion, _orchestrator.ActivityId); } private string GetPackageShortDescription() @@ -276,28 +272,6 @@ private bool IsVersioningSupported() return !_wpm.IsMsStorePackage(_package); } - /// - /// Checks if the package is selectable - /// - /// True if the package is selectable - /// Allow selecting a different version to install if the package is installed - private bool IsSelectable() - { - if (!IsInstalled) - { - return true; - } - - if (!IsVersioningSupported()) - { - return false; - } - - var isValidSelectedVersion = AvailableVersions.Contains(SelectedVersion); - var isNotInstalledVersion = SelectedVersion != InstalledVersion; - return isValidSelectedVersion && isNotInstalledVersion; - } - /// /// Get the default selected version /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs index e9d36e972..7f9ef63fa 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs @@ -5,15 +5,18 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Extensions; +using DevHome.Common.Windows.FileDialog; using DevHome.SetupFlow.Models; using DevHome.SetupFlow.Services; using DevHome.SetupFlow.TaskGroups; -using Microsoft.Extensions.Hosting; using Serilog; +using WinUIEx; namespace DevHome.SetupFlow.ViewModels; @@ -21,9 +24,9 @@ public partial class ReviewViewModel : SetupPageViewModelBase { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ReviewViewModel)); - private readonly IHost _host; - private readonly SetupFlowOrchestrator _setupFlowOrchestrator; + private readonly ConfigurationFileBuilder _configFileBuilder; + private readonly WindowEx _mainWindow; [ObservableProperty] private IList _reviewTabs; @@ -74,18 +77,21 @@ public bool CanSetupTarget public bool HasTasksToSetUp => Orchestrator.TaskGroups.Any(g => g.SetupTasks.Any()); + public bool HasDSCTasksToDownload => Orchestrator.TaskGroups.Any(g => g.DSCTasks.Any()); + public ReviewViewModel( ISetupFlowStringResource stringResource, SetupFlowOrchestrator orchestrator, - IHost host) + ConfigurationFileBuilder configFileBuilder, + WindowEx mainWindow) : base(stringResource, orchestrator) { - _host = host; - NextPageButtonText = StringResource.GetLocalized(StringResourceKey.SetUpButton); PageTitle = StringResource.GetLocalized(StringResourceKey.ReviewPageTitle); _setupFlowOrchestrator = orchestrator; + _configFileBuilder = configFileBuilder; + _mainWindow = mainWindow; } protected async override Task OnEachNavigateToAsync() @@ -138,5 +144,29 @@ private async Task OnSetUpAsync() { _log.Error($"Failed to initialize elevated process.", e); } - } + } + + [RelayCommand(CanExecute = nameof(HasDSCTasksToDownload))] + private async Task DownloadConfigurationAsync() + { + try + { + // Show the save file dialog + using var fileDialog = new WindowSaveFileDialog(); + fileDialog.AddFileType(StringResource.GetLocalized(StringResourceKey.FilePickerSingleFileTypeOption, "YAML"), ".winget"); + fileDialog.AddFileType(StringResource.GetLocalized(StringResourceKey.FilePickerSingleFileTypeOption, "YAML"), ".dsc.yaml"); + var fileName = fileDialog.Show(_mainWindow); + + // If the user selected a file, write the configuration to it + if (!string.IsNullOrEmpty(fileName)) + { + var configFile = _configFileBuilder.BuildConfigFileStringFromTaskGroups(Orchestrator.TaskGroups, ConfigurationFileKind.Normal); + await File.WriteAllTextAsync(fileName, configFile); + } + } + catch (Exception e) + { + _log.Error($"Failed to download configuration file.", e); + } + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs index 4b552efce..884831843 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs @@ -100,7 +100,7 @@ public ObservableCollection AppsDownloaded get { var packagesInstalled = new ObservableCollection(); - var packages = _packageProvider.SelectedPackages.Where(sp => sp.InstallPackageTask.WasInstallSuccessful == true).ToList(); + var packages = _packageProvider.SelectedPackages.Where(sp => sp.CanInstall && sp.InstallPackageTask.WasInstallSuccessful).ToList(); packages.ForEach(p => packagesInstalled.Add(p)); var localizedHeader = (packagesInstalled.Count == 1) ? StringResourceKey.SummaryPageOneApplicationInstalled : StringResourceKey.SummaryPageAppsDownloadedCount; ApplicationsClonedText = StringResource.GetLocalized(localizedHeader); @@ -275,7 +275,7 @@ private async Task ReloadCatalogsAsync() // After installing packages, reconnect to catalogs to // reflect the latest changes when new Package COM objects are created _log.Information($"Checking if a new catalog connections should be established"); - if (_packageProvider.SelectedPackages.Any(package => package.InstallPackageTask.WasInstallSuccessful)) + if (_packageProvider.SelectedPackages.Any(package => package.CanInstall && package.InstallPackageTask.WasInstallSuccessful)) { await _appManagementInitializer.ReinitializeAsync(); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml index a87cb0358..f4552ef5c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AppManagementView.xaml @@ -28,6 +28,12 @@ + + + + + + @@ -38,7 +44,7 @@ - @@ -55,7 +61,7 @@ HorizontalAlignment="Left" Width="400" Margin="0,0,0,20" - x:Uid="ms-resource:///DevHome.SetupFlow/Resources/SearchBox"> + x:Uid="SearchBox"> @@ -99,43 +105,59 @@ - + + - + + + + + + - + - - + + - + + + + + + - + - + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml index 4f36487af..210a2d76c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/PackageView.xaml @@ -15,7 +15,6 @@ - @@ -46,7 +45,6 @@ - @@ -74,7 +72,6 @@