From 8f6576f0fb3112cb57058e4974d1a1814e6435c1 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 15 Feb 2024 19:22:25 +1100 Subject: [PATCH 01/19] Bookmarks dialog views infrastructure (#2198) Task/Issue URL: https://app.asana.com/0/0/1206531304671947/f **Description**: Extracted views to build Bookmark dialog forms in a more composable way. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++ .../BookmarksFolder.svg | 10 + .../BookmarksFolder.imageset/Contents.json | 12 ++ .../Bookmarks/View/BookmarkFolderPicker.swift | 2 +- .../Dialog/AddEditBookmarkDialogView.swift | 174 +++++++++++++++++ .../Dialog/BookmarkDialogButtonsView.swift | 176 ++++++++++++++++++ .../Dialog/BookmarkDialogContainerView.swift | 50 +++++ .../BookmarkDialogFolderManagementView.swift | 76 ++++++++ .../BookmarkDialogStackedContentView.swift | 105 +++++++++++ .../View/Dialog/BookmarkFavoriteView.swift | 47 +++++ .../{ => Dialogs}/Dialog.swift | 0 .../Dialogs/TieredDialogView.swift | 70 +++++++ .../TwoColumnsListView.swift | 62 ++++++ 13 files changed, 839 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/BookmarksFolder.svg create mode 100644 DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift rename LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/{ => Dialogs}/Dialog.swift (100%) create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ebc0323dd8..0afc4c0478 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2417,6 +2417,24 @@ 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; + 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; + 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E02B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E12B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */; }; + 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E52B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */; }; + 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173E92B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */; }; + 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = AA06B6B62672AF8100F541C5 /* Sparkle */; }; AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */; }; AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */; }; @@ -3992,6 +4010,12 @@ 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionTests.swift; sourceTree = ""; }; 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtensionTests.swift; sourceTree = ""; }; + 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; + 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogButtonsView.swift; sourceTree = ""; }; + 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; + 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; + 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; + 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFavoriteView.swift; sourceTree = ""; }; AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariVersionReaderTests.swift; sourceTree = ""; }; AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProviderTests.swift; sourceTree = ""; }; AA0F3DB6261A566C0077F2D9 /* SuggestionLoadingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionLoadingMock.swift; sourceTree = ""; }; @@ -6648,6 +6672,19 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9FA173DD2B7A0ECE00EE4E6E /* Dialog */ = { + isa = PBXGroup; + children = ( + 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */, + 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */, + 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */, + 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */, + 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */, + 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */, + ); + path = Dialog; + sourceTree = ""; + }; AA0877B626D515EE00B05660 /* UserAgent */ = { isa = PBXGroup; children = ( @@ -7473,6 +7510,7 @@ AAC5E4C125D6A6C3007F5990 /* View */ = { isa = PBXGroup; children = ( + 9FA173DD2B7A0ECE00EE4E6E /* Dialog */, 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */, 7BEC20412B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift */, 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */, @@ -9794,6 +9832,7 @@ 4B9DB0332A983B24000927DB /* EnableWaitlistFeatureView.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, + 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, 3706FABF293F65D500E42796 /* CrashReportReader.swift in Sources */, @@ -9855,6 +9894,7 @@ 3706FAF1293F65D500E42796 /* PreferencesAboutView.swift in Sources */, 3706FAF2293F65D500E42796 /* ContentBlocking.swift in Sources */, 31F2D2002AF026D800BF0144 /* WaitlistTermsAndConditionsActionHandler.swift in Sources */, + 9FA173E02B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, 3706FAF3293F65D500E42796 /* LocalAuthenticationService.swift in Sources */, 1D36E659298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, B6BCC5242AFCDABB002C5499 /* DataImportSourceViewModel.swift in Sources */, @@ -10098,6 +10138,7 @@ 3706FB9E293F65D500E42796 /* AboutModel.swift in Sources */, 3706FB9F293F65D500E42796 /* PasswordManagementCreditCardItemView.swift in Sources */, 3706FBA0293F65D500E42796 /* NSTextFieldExtension.swift in Sources */, + 9FA173E82B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 3706FBA1293F65D500E42796 /* FireproofDomainsContainer.swift in Sources */, 3706FBA2293F65D500E42796 /* GeolocationService.swift in Sources */, 4B4D60C42A0C849600BCD287 /* NetworkProtectionInvitePresenter.swift in Sources */, @@ -10187,6 +10228,7 @@ EEC4A6722B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 3706FBE0293F65D500E42796 /* NSException+Catch.m in Sources */, 3706FBE1293F65D500E42796 /* AppStateRestorationManager.swift in Sources */, + 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 3706FBE2293F65D500E42796 /* ClickToLoadUserScript.swift in Sources */, 3706FBE3293F65D500E42796 /* WindowControllersManager.swift in Sources */, 37197EAA2942443D00394917 /* ModalSheetCancellable.swift in Sources */, @@ -10251,6 +10293,7 @@ B66CA41F2AD910B300447CF0 /* DataImportView.swift in Sources */, 3706FC0C293F65D500E42796 /* NSAttributedStringExtension.swift in Sources */, 3706FC0D293F65D500E42796 /* AnimationView.swift in Sources */, + 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 3706FC0E293F65D500E42796 /* NSRectExtension.swift in Sources */, 3706FC0F293F65D500E42796 /* YoutubeOverlayUserScript.swift in Sources */, 3775913729AB9A1C00E26367 /* SyncManagementDialogViewController.swift in Sources */, @@ -10381,6 +10424,7 @@ 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, 3706FC76293F65D500E42796 /* PixelDataRecord.swift in Sources */, 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, + 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */, @@ -10915,6 +10959,7 @@ 4B9579552AC7AE700062CA31 /* Logging.swift in Sources */, 4B9579562AC7AE700062CA31 /* CrashReportPromptPresenter.swift in Sources */, B6B4D1CD2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, + 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 4B9579572AC7AE700062CA31 /* BWCredential.swift in Sources */, 4B9579582AC7AE700062CA31 /* PreferencesRootView.swift in Sources */, 4B9579592AC7AE700062CA31 /* AppStateChangedPublisher.swift in Sources */, @@ -11321,6 +11366,7 @@ 4B957AC42AC7AE700062CA31 /* BWVault.swift in Sources */, 4B957AC52AC7AE700062CA31 /* NSViewExtension.swift in Sources */, BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, + 9FA173E52B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 4B957AC72AC7AE700062CA31 /* DownloadListViewModel.swift in Sources */, 4B957AC82AC7AE700062CA31 /* BookmarkManagementDetailViewController.swift in Sources */, 4B957AC92AC7AE700062CA31 /* CSVImporter.swift in Sources */, @@ -11341,6 +11387,7 @@ 4B957AD72AC7AE700062CA31 /* CustomRoundedCornersShape.swift in Sources */, 4B957AD82AC7AE700062CA31 /* LocaleExtension.swift in Sources */, 4B957AD92AC7AE700062CA31 /* SavePaymentMethodViewController.swift in Sources */, + 9FA173E92B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, 4B957ADA2AC7AE700062CA31 /* BWStatus.swift in Sources */, 4B957ADB2AC7AE700062CA31 /* WebKitVersionProvider.swift in Sources */, B6BCC54D2AFDF24B002C5499 /* TaskWithProgress.swift in Sources */, @@ -11368,6 +11415,7 @@ 4B957AF02AC7AE700062CA31 /* NSException+Catch.m in Sources */, 4B957AF12AC7AE700062CA31 /* AppStateRestorationManager.swift in Sources */, 4B957AF22AC7AE700062CA31 /* DailyPixel.swift in Sources */, + 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, 4B957AF32AC7AE700062CA31 /* NavigationHotkeyHandler.swift in Sources */, 1EA7B8DA2B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 4B957AF42AC7AE700062CA31 /* ClickToLoadUserScript.swift in Sources */, @@ -11494,6 +11542,7 @@ 4B957B612AC7AE700062CA31 /* HomePage.swift in Sources */, 4B957B622AC7AE700062CA31 /* RoundedSelectionRowView.swift in Sources */, B6A22B652B1E29D000ECD2BA /* DataImportSummaryViewModel.swift in Sources */, + 9FA173E12B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, 4B957B632AC7AE700062CA31 /* LocalStatisticsStore.swift in Sources */, 4B957B642AC7AE700062CA31 /* BackForwardListItem.swift in Sources */, 4B957B672AC7AE700062CA31 /* AtbAndVariantCleanup.swift in Sources */, @@ -11568,6 +11617,7 @@ 4B957BA12AC7AE700062CA31 /* UserDefaults+NetworkProtectionShared.swift in Sources */, 4B957BA22AC7AE700062CA31 /* NavigationActionPolicyExtension.swift in Sources */, 4B957BA32AC7AE700062CA31 /* CIImageExtension.swift in Sources */, + 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 4B957BA42AC7AE700062CA31 /* NSMenuExtension.swift in Sources */, 4B957BA52AC7AE700062CA31 /* MainWindowController.swift in Sources */, 4B957BA62AC7AE700062CA31 /* Tab.swift in Sources */, @@ -12026,6 +12076,7 @@ AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */, 7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */, 370A34B12AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */, + 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, AAC5E4C725D6A6E8007F5990 /* AddBookmarkPopover.swift in Sources */, 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */, B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */, @@ -12141,6 +12192,7 @@ 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, 9812D895276CEDA5004B6181 /* ContentBlockerRulesLists.swift in Sources */, 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */, + 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */, 4B3B8490297A0E1000A384BD /* EmailManagerExtension.swift in Sources */, B64C84F1269310120048FEBE /* PermissionManager.swift in Sources */, @@ -12207,6 +12259,7 @@ 4BB99D0026FE191E001E4761 /* CoreDataBookmarkImporter.swift in Sources */, 4BCF15D72ABB8A110083F6DF /* NetworkProtectionRemoteMessaging.swift in Sources */, C168B9AC2B31DC7E001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, + 9FA173E72B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift in Sources */, D64A5FF82AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */, AA3F895324C18AD500628DDE /* SuggestionViewModel.swift in Sources */, @@ -12255,6 +12308,7 @@ 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */, B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */, 4B41EDA72B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, + 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 373A1AA8283ED1B900586521 /* BookmarkHTMLReader.swift in Sources */, B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */, 4B37EE612B4CFC3C00A89A61 /* SurveyURLBuilder.swift in Sources */, @@ -12316,6 +12370,7 @@ 4B4D60BF2A0C848A00BCD287 /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 3158B1592B0BF76400AF130C /* DataBrokerProtectionFeatureDisabler.swift in Sources */, B655124829A79465009BFE1C /* NavigationActionExtension.swift in Sources */, + 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */, 85308E25267FC9F2001ABD76 /* NSAlertExtension.swift in Sources */, B69A14F62B4D701F00B9417D /* AddBookmarkPopoverViewModel.swift in Sources */, 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */, @@ -12397,6 +12452,7 @@ AA72D5FE25FFF94E00C77619 /* NSMenuItemExtension.swift in Sources */, 4BA1A6C2258B0A1300F6F690 /* ContiguousBytesExtension.swift in Sources */, B6A22B622B1E29D000ECD2BA /* DataImportSummaryViewModel.swift in Sources */, + 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */, 1EA7B8D82B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, 987799F62999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/BookmarksFolder.svg b/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/BookmarksFolder.svg new file mode 100644 index 0000000000..4487eeba1a --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/BookmarksFolder.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json new file mode 100644 index 0000000000..aee9d2b2fb --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/BookmarksFolder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "BookmarksFolder.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift b/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift index c0aeab9bec..e7de655e8c 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkFolderPicker.swift @@ -32,7 +32,7 @@ struct BookmarkFolderPicker: View { return popUpButton } content: { - PopupButtonItem(icon: .folder, title: UserText.bookmarks) + PopupButtonItem(icon: .bookmarksFolder, title: UserText.bookmarks) PopupButtonItem.separator() diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift new file mode 100644 index 0000000000..a568549a45 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -0,0 +1,174 @@ +// +// AddEditBookmarkDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine +import Common + +@MainActor +final class AddEditBookmarkDialogViewModel: ObservableObject { + + enum Mode { + case add + case edit + } + + @Published var bookmarkName: String + @Published var bookmarkURLPath: String + @Published var isBookmarkFavorite: Bool + @Published var folders: [FolderViewModel] = [] + @Published var selectedFolder: BookmarkFolder? + + let title: String = "" + let defaultActionTitle = "" + var isDefaultActionButtonDisabled: Bool { false } + + private let bookmark: Bookmark + private let mode: Mode + private let bookmarkManager: BookmarkManager + + init( + mode: Mode, + bookmark: Bookmark, + bookmarkManager: LocalBookmarkManager = .shared + ) { + self.bookmark = bookmark + self.mode = mode + self.bookmarkManager = bookmarkManager + bookmarkName = bookmark.title + bookmarkURLPath = bookmark.url + isBookmarkFavorite = bookmark.isFavorite + } + + func cancelAction(dismiss: () -> Void) {} + func saveOrAddAction(dismiss: () -> Void) {} + func addFolderButtonAction() {} +} + +struct AddEditBookmarkDialogView: ModalView { + @ObservedObject private var viewModel: AddEditBookmarkDialogViewModel + + init(viewModel: AddEditBookmarkDialogViewModel) { + self.viewModel = viewModel + } + + var body: some View { + BookmarkDialogContainerView( + title: viewModel.title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $viewModel.bookmarkName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.url, + content: TextField("", text: $viewModel.bookmarkURLPath) + .accessibilityIdentifier("bookmark.add.url.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: viewModel.folders, + selectedFolder: $viewModel.selectedFolder, + onActionButton: viewModel.addFolderButtonAction + ) + ) + ) + BookmarkFavoriteView(isFavorite: $viewModel.isBookmarkFavorite) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: UserText.cancel, + action: viewModel.cancelAction, + keyboardShortCut: .cancelAction + ), defaultButtonAction: .init( + title: viewModel.defaultActionTitle, + action: viewModel.saveOrAddAction, + keyboardShortCut: .defaultAction + ), + shouldDisableDefaultButtonAction: viewModel.isDefaultActionButtonDisabled + ) + } + ) + .font(.system(size: 13)) + .frame(width: 448, height: 288) + } +} + +// MARK: - AddEditBookmarkDialogViewModel.Mode + +private extension AddEditBookmarkDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addBookmark + case .edit: + return UserText.Bookmarks.Dialog.Title.editBookmark + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addBookmark + case .edit: + return UserText.save + } + } + +} + +// MARK: - Previews + +#Preview("Add Bookmark - Light Mode") { + let bookmark = Bookmark(id: "1", url: "", title: "", isFavorite: false) + let viewModel = AddEditBookmarkDialogViewModel(mode: .add, bookmark: bookmark) + return AddEditBookmarkDialogView(viewModel: viewModel) + .preferredColorScheme(.light) +} + +#Preview("Edit Bookmark - Light Mode") { + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true) + let viewModel = AddEditBookmarkDialogViewModel(mode: .edit, bookmark: bookmark) + return AddEditBookmarkDialogView(viewModel: viewModel) + .preferredColorScheme(.light) +} + +#Preview("Add Bookmark - Dark Mode") { + let bookmark = Bookmark(id: "1", url: "", title: "", isFavorite: false) + let viewModel = AddEditBookmarkDialogViewModel(mode: .add, bookmark: bookmark) + return AddEditBookmarkDialogView(viewModel: viewModel) + .preferredColorScheme(.dark) +} + +#Preview("Edit Bookmark - Dark Mode") { + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true) + let viewModel = AddEditBookmarkDialogViewModel(mode: .edit, bookmark: bookmark) + return AddEditBookmarkDialogView(viewModel: viewModel) + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift new file mode 100644 index 0000000000..26c9c7284e --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift @@ -0,0 +1,176 @@ +// +// BookmarkDialogButtonsView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct BookmarkDialogButtonsView: View { + private let viewState: ViewState + private let otherButtonAction: Action + private let defaultButtonAction: Action + private let shouldDisableDefaultButtonAction: Bool + @Environment(\.dismiss) private var dismiss + + init( + viewState: ViewState, + otherButtonAction: Action, + defaultButtonAction: Action, + shouldDisableDefaultButtonAction: Bool + ) { + self.viewState = viewState + self.otherButtonAction = otherButtonAction + self.defaultButtonAction = defaultButtonAction + self.shouldDisableDefaultButtonAction = shouldDisableDefaultButtonAction + } + + var body: some View { + HStack { + if viewState == .compressed { + Spacer() + } + + actionButton(action: otherButtonAction, viewState: viewState) + + actionButton(action: defaultButtonAction, viewState: viewState) + .disabled(shouldDisableDefaultButtonAction) + } + } + + @MainActor + private func actionButton(action: Action, viewState: ViewState) -> some View { + Button { + action.action(dismiss.callAsFunction) + } label: { + Text(action.title) + .frame(height: viewState.height) + .frame(maxWidth: viewState.maxWidth) + } + .keyboardShortcut(action.keyboardShortCut) + } +} + +// MARK: - BookmarkDialogButtonsView + Types + +extension BookmarkDialogButtonsView { + + enum ViewState: Equatable { + case compressed + case expanded + } + + struct Action { + let title: String + let action: @MainActor (_ dismiss: () -> Void) -> Void + let keyboardShortCut: KeyboardShortcut? + + init(title: String, action: @MainActor @escaping (_ dismiss: () -> Void) -> Void, keyboardShortCut: KeyboardShortcut? = nil) { + self.title = title + self.action = action + self.keyboardShortCut = keyboardShortCut + } + } +} + +// MARK: - BookmarkDialogButtonsView.ViewState + +private extension BookmarkDialogButtonsView.ViewState { + + var maxWidth: CGFloat? { + switch self { + case .compressed: + return nil + case .expanded: + return .infinity + } + } + + var height: CGFloat? { + switch self { + case .compressed: + return nil + case .expanded: + return 28.0 + } + } + +} + +// MARK: - Preview + +#Preview("Compressed - Disable Default Button") { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + action: {_ in } + ), + shouldDisableDefaultButtonAction: true + ) + .frame(width: 320, height: 50) +} + +#Preview("Compressed - Enabled Default Button") { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + action: {_ in } + ), + shouldDisableDefaultButtonAction: false + ) + .frame(width: 320, height: 50) +} + +#Preview("Expanded - Disable Default Button") { + BookmarkDialogButtonsView( + viewState: .expanded, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + action: {_ in } + ), + shouldDisableDefaultButtonAction: true + ) + .frame(width: 320, height: 50) +} + +#Preview("Expanded - Enable Default Button") { + BookmarkDialogButtonsView( + viewState: .expanded, + otherButtonAction: .init( + title: "Left", + action: { _ in } + ), + defaultButtonAction: .init( + title: "Right", + action: {_ in } + ), + shouldDisableDefaultButtonAction: false + ) + .frame(width: 320, height: 50) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift new file mode 100644 index 0000000000..ea49712abb --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogContainerView.swift @@ -0,0 +1,50 @@ +// +// BookmarkDialogContainerView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogContainerView: View { + private let title: String + @ViewBuilder private let middleSection: () -> Content + @ViewBuilder private let bottomSection: () -> Buttons + + init( + title: String, + @ViewBuilder middleSection: @escaping () -> Content, + @ViewBuilder bottomSection: @escaping () -> Buttons + ) { + self.title = title + self.middleSection = middleSection + self.bottomSection = bottomSection + } + + var body: some View { + TieredDialogView( + verticalSpacing: 16.0, + horizontalPadding: 20.0, + top: { + Text(title) + .foregroundColor(.primary) + .fontWeight(.semibold) + }, + center: middleSection, + bottom: bottomSection + ) + } +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift new file mode 100644 index 0000000000..8081abc14d --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogFolderManagementView.swift @@ -0,0 +1,76 @@ +// +// BookmarkDialogFolderManagementView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogFolderManagementView: View { + private let folders: [FolderViewModel] + private var selectedFolder: Binding + private let onActionButton: @MainActor () -> Void + + init( + folders: [FolderViewModel], + selectedFolder: Binding, + onActionButton: @escaping @MainActor () -> Void + ) { + self.folders = folders + self.selectedFolder = selectedFolder + self.onActionButton = onActionButton + } + + var body: some View { + HStack { + BookmarkFolderPicker( + folders: folders, + selectedFolder: selectedFolder + ) + .accessibilityIdentifier("bookmark.add.folder.dropdown") + + Button { + onActionButton() + } label: { + Image(.addFolder) + } + .accessibilityIdentifier("bookmark.add.new.folder.button") + .buttonStyle(StandardButtonStyle()) + } + } +} + +#Preview { + @State var selectedFolder: BookmarkFolder? = BookmarkFolder(id: "1", title: "Nested Folder", children: []) + let folderViewModels: [FolderViewModel] = [ + .init( + entity: .init( + id: "1", + title: "Nested Folder", + parentFolderUUID: nil, + children: [] + ), + level: 1 + ) + ] + + return BookmarkDialogFolderManagementView( + folders: folderViewModels, + selectedFolder: $selectedFolder, + onActionButton: {} + ) + .frame(width: 400) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift new file mode 100644 index 0000000000..df63a94090 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift @@ -0,0 +1,105 @@ +// +// BookmarkDialogStackedContentView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions + +struct BookmarkDialogStackedContentView: View { + private let items: [Item] + + init(_ items: Item...) { + self.items = items + } + + init(_ items: [Item]) { + self.items = items + } + + var body: some View { + TwoColumnsListView( + horizontalSpacing: 16.0, + verticalSpacing: 20.0, + rowHeight: 22.0, + leftColumn: { + ForEach(items, id: \.title) { item in + Text(item.title) + .foregroundColor(.primary) + .fontWeight(.medium) + } + }, + rightColumn: { + ForEach(items, id: \.title) { item in + item.content + } + } + ) + } +} + +// MARK: - BookmarkModalStackedContentView + Item + +extension BookmarkDialogStackedContentView { + struct Item { + fileprivate let title: String + fileprivate let content: AnyView + + init(title: String, content: any View) { + self.title = title + self.content = AnyView(content) + } + } +} + +// MARK: - Preview + +#Preview { + @State var name: String = "DuckDuckGo" + @State var url: String = "https://www.duckduckgo.com" + @State var selectedFolder: BookmarkFolder? + + return BookmarkDialogStackedContentView( + .init( + title: "Name", + content: + TextField("", text: $name) + .textFieldStyle(.roundedBorder) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + + ), + .init( + title: "URL", + content: + TextField("", text: $url) + .textFieldStyle(.roundedBorder) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: "Location", + content: + BookmarkDialogFolderManagementView( + folders: [], + selectedFolder: $selectedFolder, + onActionButton: { } + ) + ) + ) + .padding([.horizontal, .vertical]) + .frame(width: 400) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift new file mode 100644 index 0000000000..9778ab0d5c --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkFavoriteView.swift @@ -0,0 +1,47 @@ +// +// BookmarkFavoriteView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import SwiftUIExtensions +import PreferencesViews + +struct BookmarkFavoriteView: View { + @Binding var isFavorite: Bool + + var body: some View { + Toggle(isOn: $isFavorite) { + HStack(spacing: 6) { + Image(.favoriteFilledBorder) + Text(UserText.addToFavorites) + .foregroundColor(.primary) + } + } + .toggleStyle(.checkbox) + .accessibilityIdentifier("bookmark.add.add.to.favorites.button") + } +} + +#Preview("Favorite") { + BookmarkFavoriteView(isFavorite: .constant(true)) + .frame(width: 300) +} + +#Preview("Not Favorite") { + BookmarkFavoriteView(isFavorite: .constant(false)) + .frame(width: 300) +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialog.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/Dialog.swift similarity index 100% rename from LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialog.swift rename to LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/Dialog.swift diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift new file mode 100644 index 0000000000..541941304b --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/Dialogs/TieredDialogView.swift @@ -0,0 +1,70 @@ +// +// TieredDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A view to arrange its subviews in a three vertical sections separated by dividers. +public struct TieredDialogView: View { + private let verticalSpacing: CGFloat + private let horizontalAlignment: HorizontalAlignment + private let horizontalPadding: CGFloat? + @ViewBuilder private let top: () -> Top + @ViewBuilder private let center: () -> Center + @ViewBuilder private let bottom: () -> Bottom + + /// Creates an instance with the given vertical spacing, horizontal alignment, horizontal padding and views created by the specified view builders. + /// - Parameters: + /// - verticalSpacing: The distance between adjacent sections. + /// - horizontalAlignment: The guide for aligning the sections in the vertical stack. This guide has the same vertical screen coordinate for every subview. + /// - horizontalPadding: The padding amount to add to the horizontal edges of the sections. + /// - top: A view builder that creates the content of the top section of the dialog. + /// - center: A view builder that creates the content of the central section of the dialog. + /// - bottom: A view builder that creates the content of the bottom section of the dialog. + public init( + verticalSpacing: CGFloat = 10.0, + horizontalAlignment: HorizontalAlignment = .leading, + horizontalPadding: CGFloat? = nil, + @ViewBuilder top: @escaping () -> Top, + @ViewBuilder center: @escaping () -> Center, + @ViewBuilder bottom: @escaping () -> Bottom + ) { + self.horizontalAlignment = horizontalAlignment + self.verticalSpacing = verticalSpacing + self.horizontalPadding = horizontalPadding + self.top = top + self.center = center + self.bottom = bottom + } + + public var body: some View { + VStack(alignment: horizontalAlignment, spacing: verticalSpacing) { + top() + .padding(.horizontal, horizontalPadding) + + Divider() + + center() + .padding(.horizontal, horizontalPadding) + + Divider() + + bottom() + .padding(.horizontal, horizontalPadding) + } + } +} diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift new file mode 100644 index 0000000000..55bb57bca0 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/TwoColumnsListView.swift @@ -0,0 +1,62 @@ +// +// TwoColumnsListView.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A view to arrange its subviews in two-column equally spaced rows. +public struct TwoColumnsListView: View { + private let rowHeight: CGFloat? + private let horizontalSpacing: CGFloat? + private let verticalSpacing: CGFloat? + @ViewBuilder private let leftColumn: () -> Left + @ViewBuilder private let rightColumn: () -> Right + + /// Creates an instance with the given horizontal and vertical spacing, row height and + /// - Parameters: + /// - horizontalSpacing: The horizontal distance between adjacent subviews. + /// - verticalSpacing: The vertical distance between adjacent subviews. + /// - rowHeight: The height of the rows in the stack. + /// - leftColumn: A view builder that creates the content of the left section of the view. + /// - rightColumn: A view builder that creates the content of the right section of the view. + public init( + horizontalSpacing: CGFloat? = nil, + verticalSpacing: CGFloat? = nil, + rowHeight: CGFloat? = nil, + @ViewBuilder leftColumn: @escaping () -> Left, + @ViewBuilder rightColumn: @escaping () -> Right + ) { + self.horizontalSpacing = horizontalSpacing + self.verticalSpacing = verticalSpacing + self.rowHeight = rowHeight + self.leftColumn = leftColumn + self.rightColumn = rightColumn + } + + public var body: some View { + HStack(alignment: .center, spacing: horizontalSpacing) { + VStack(alignment: .leading, spacing: verticalSpacing) { + leftColumn() + .frame(height: rowHeight) + } + VStack(alignment: .leading, spacing: verticalSpacing) { + rightColumn() + .frame(height: rowHeight) + } + } + } +} From d7354a65cf5a9324f471be9b9d2a83c25666ceb7 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 16 Feb 2024 09:55:54 +1100 Subject: [PATCH 02/19] Bookmarks add/edit dialog views (#2209) Task/Issue URL: https://app.asana.com/0/0/1206531304671946/f CC: @samsymons **Description**: 1. This PR refactors updates the design for `AddBookmarkPopoverView` and `AddBookmarkFolderPopoverView` and add `AddEditBookmarkFolderView` to add/edit a bookmark folder --- DuckDuckGo.xcodeproj/project.pbxproj | 8 + .../View/AddBookmarkFolderPopoverView.swift | 99 ++++++------ .../View/AddBookmarkPopoverView.swift | 122 ++++++--------- .../Dialog/AddEditBookmarkDialogView.swift | 12 +- .../AddEditBookmarkFolderDialogView.swift | 148 ++++++++++++++++++ .../Dialog/BookmarkDialogButtonsView.swift | 42 +++-- .../AddBookmarkPopoverViewModel.swift | 31 ++-- DuckDuckGo/Common/Localizables/UserText.swift | 2 - .../VPNLocation/VPNLocationView.swift | 15 -- .../View+ConditionalModifiers.swift | 47 ++++++ 10 files changed, 356 insertions(+), 170 deletions(-) create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift create mode 100644 LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0afc4c0478..4b386fcdfb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2417,6 +2417,9 @@ 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; + 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; @@ -4010,6 +4013,7 @@ 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionTests.swift; sourceTree = ""; }; 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtensionTests.swift; sourceTree = ""; }; + 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogView.swift; sourceTree = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogButtonsView.swift; sourceTree = ""; }; 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; @@ -6681,6 +6685,7 @@ 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */, 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */, 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */, + 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */, ); path = Dialog; sourceTree = ""; @@ -10378,6 +10383,7 @@ 3706FC50293F65D500E42796 /* FeedbackWindow.swift in Sources */, 3706FC51293F65D500E42796 /* RecentlyVisitedView.swift in Sources */, B645D8F729FA95440024461F /* WKProcessPoolExtension.swift in Sources */, + 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 3706FC52293F65D500E42796 /* MouseOverAnimationButton.swift in Sources */, 3706FC53293F65D500E42796 /* TabBarScrollView.swift in Sources */, 3706FC54293F65D500E42796 /* BookmarkListTreeControllerDataSource.swift in Sources */, @@ -11001,6 +11007,7 @@ 4B9579792AC7AE700062CA31 /* BWRequest.swift in Sources */, 4B95797A2AC7AE700062CA31 /* WKWebViewConfigurationExtensions.swift in Sources */, 4B95797B2AC7AE700062CA31 /* HomePageDefaultBrowserModel.swift in Sources */, + 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 4B95797C2AC7AE700062CA31 /* CrashReporter.swift in Sources */, 4B95797D2AC7AE700062CA31 /* AddressBarTextSelectionNavigation.swift in Sources */, 4B37EE7D2B4CFF8300A89A61 /* SurveyURLBuilder.swift in Sources */, @@ -12452,6 +12459,7 @@ AA72D5FE25FFF94E00C77619 /* NSMenuItemExtension.swift in Sources */, 4BA1A6C2258B0A1300F6F690 /* ContiguousBytesExtension.swift in Sources */, B6A22B622B1E29D000ECD2BA /* DataImportSummaryViewModel.swift in Sources */, + 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */, 1EA7B8D82B7E1283000330A4 /* SubscriptionFeatureAvailability.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift index a0ab50c89a..c0c3ae17d3 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift @@ -23,59 +23,54 @@ struct AddBookmarkFolderPopoverView: ModalView { @ObservedObject var model: AddBookmarkFolderPopoverViewModel var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(UserText.newFolder) - .bold() - - VStack(alignment: .leading, spacing: 7) { - Text("Location:", comment: "Add Folder popover: parent folder picker title") - - BookmarkFolderPicker(folders: model.folders, selectedFolder: $model.parent) - .accessibilityIdentifier("bookmark.folder.folder.dropdown") - .disabled(model.isDisabled) - } - - VStack(alignment: .leading, spacing: 7) { - Text(UserText.newFolderDialogFolderNameTitle) - - TextField("", text: $model.folderName) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.folder.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .disabled(model.isDisabled) + BookmarkDialogContainerView( + title: UserText.Bookmarks.Dialog.Title.addFolder, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $model.folderName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.folder.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + .disabled(model.isDisabled) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkFolderPicker(folders: model.folders, selectedFolder: $model.parent) + .accessibilityIdentifier("bookmark.folder.folder.dropdown") + .disabled(model.isDisabled) + ) + ) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .expanded, + otherButtonAction: .init( + title: UserText.cancel, + accessibilityIdentifier: "bookmark.add.cancel.button", + isDisabled: model.isDisabled, + action: { _ in model.cancel() } + ), + defaultButtonAction: .init( + title: UserText.Bookmarks.Dialog.Action.addFolder, + accessibilityIdentifier: "bookmark.add.add.folder.button", + keyboardShortCut: .defaultAction, + isDisabled: model.isAddFolderButtonDisabled || model.isDisabled, + action: { _ in model.addFolder() } + ) + ) } - .padding(.bottom, 16) - - HStack { - Spacer() - - Button(action: { - model.cancel() - }) { - Text(UserText.cancel) - } - .accessibilityIdentifier("bookmark.add.cancel.button") - .disabled(model.isDisabled) - - Button(action: { - model.addFolder() - }) { - Text("Add Folder", comment: "Add Folder popover: Create folder button") - } - .keyboardShortcut(.defaultAction) - .accessibilityIdentifier("bookmark.add.add.folder.button") - .disabled(model.isAddFolderButtonDisabled || model.isDisabled) - } - } + ) + .padding(.vertical, 16.0) .font(.system(size: 13)) - .padding() - .frame(width: 300, height: 229) - .background(Color(.popoverBackground)) + .frame(width: 320) } } #if DEBUG -#Preview { +#Preview("Add Folder - Light") { let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ BookmarkFolder(id: "1", title: "Folder 1", children: [ BookmarkFolder(id: "2", title: "Nested Folder with a name that in theory won‘t fit into the picker", children: [ @@ -94,5 +89,15 @@ struct AddBookmarkFolderPopoverView: ModalView { return AddBookmarkFolderPopoverView(model: AddBookmarkFolderPopoverViewModel(bookmarkManager: bkman) { print("CompletionHandler:", $0?.title ?? "") }) + .preferredColorScheme(.light) +} + +#Preview("Add Folder - Dark") { + let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + + return AddBookmarkFolderPopoverView(model: AddBookmarkFolderPopoverViewModel(bookmarkManager: bkman) { + print("CompletionHandler:", $0?.title ?? "") + }) + .preferredColorScheme(.dark) } #endif diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index c8d9cadcbd..2582b61691 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -38,82 +38,54 @@ struct AddBookmarkPopoverView: View { @MainActor private var addBookmarkView: some View { - VStack(alignment: .leading, spacing: 19) { - Text("Bookmark Added", comment: "Bookmark Added popover title") - .fontWeight(.bold) - .padding(.bottom, 4) - - VStack(alignment: .leading, spacing: 10) { - TextField("", text: $model.bookmarkTitle) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.add.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - - HStack { - BookmarkFolderPicker(folders: model.folders, - selectedFolder: $model.selectedFolder) - .accessibilityIdentifier("bookmark.add.folder.dropdown") - - Button { - model.addFolderButtonAction() - } label: { - Image(.addFolder) - } - .accessibilityIdentifier("bookmark.add.new.folder.button") - .buttonStyle(StandardButtonStyle()) - } - } - - Divider() - - Button { - model.favoritesButtonAction() - } label: { - HStack(spacing: 8) { - if model.bookmark.isFavorite { - Image(.favoriteFilled) - Text(UserText.removeFromFavorites) - } else { - Image(.favorite) - Text(UserText.addToFavorites) - } - } - } - .accessibilityIdentifier("bookmark.add.add.to.favorites.button") - .buttonStyle(.borderless) - .foregroundColor(Color.button) - - HStack { - Spacer() - - Button { - model.removeButtonAction(dismiss: dismiss.callAsFunction) - } label: { - Text("Remove", comment: "Remove bookmark button title") - } - .accessibilityIdentifier("bookmark.add.remove.button") - - Button { - model.doneButtonAction(dismiss: dismiss.callAsFunction) - } label: { - Text(UserText.done) - } - .keyboardShortcut(.defaultAction) - .accessibilityIdentifier("bookmark.add.done.button") + BookmarkDialogContainerView( + title: UserText.Bookmarks.Dialog.Title.addedBookmark, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $model.bookmarkTitle) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: model.folders, + selectedFolder: $model.selectedFolder, + onActionButton: model.addFolderButtonAction + ) + ) + ) + BookmarkFavoriteView(isFavorite: $model.isBookmarkFavorite) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .expanded, + otherButtonAction: .init( + title: UserText.remove, + action: model.removeButtonAction + ), + defaultButtonAction: .init( + title: UserText.done, + keyboardShortCut: .defaultAction, + isDisabled: model.isDefaultActionButtonDisabled, + action: model.doneButtonAction + ) + ) } - - } + ) + .padding(.vertical, 16.0) .font(.system(size: 13)) - .padding(EdgeInsets(top: 19, leading: 19, bottom: 19, trailing: 19)) - .frame(width: 300, height: 229) - .background(Color(.popoverBackground)) + .frame(width: 320) } } #if DEBUG -#Preview { { +#Preview("Bookmark Added - Light") { let bkm = Bookmark(id: "n", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false, parentFolderUUID: "1") let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ BookmarkFolder(id: "1", title: "Folder with a name that shouldn‘t fit into the picker", children: [ @@ -133,5 +105,15 @@ struct AddBookmarkPopoverView: View { customAssertionFailure = { _, _, _ in } return AddBookmarkPopoverView(model: AddBookmarkPopoverViewModel(bookmark: bkm, bookmarkManager: bkman)) -}() } + .preferredColorScheme(.light) +} + +#Preview("Bookmark Added - Dark") { + let bkm = Bookmark(id: "n", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false, parentFolderUUID: "1") + let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ + BookmarkFolder(id: "1", title: "Folder with a name that shouldn‘t fit into the picker", children: [])])) + + return AddBookmarkPopoverView(model: AddBookmarkPopoverViewModel(bookmark: bkm, bookmarkManager: bkman)) + .preferredColorScheme(.dark) +} #endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift index a568549a45..6d478c5527 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -103,14 +103,14 @@ struct AddEditBookmarkDialogView: ModalView { viewState: .compressed, otherButtonAction: .init( title: UserText.cancel, - action: viewModel.cancelAction, - keyboardShortCut: .cancelAction + keyboardShortCut: .cancelAction, + action: viewModel.cancelAction ), defaultButtonAction: .init( title: viewModel.defaultActionTitle, - action: viewModel.saveOrAddAction, - keyboardShortCut: .defaultAction - ), - shouldDisableDefaultButtonAction: viewModel.isDefaultActionButtonDisabled + keyboardShortCut: .defaultAction, + isDisabled: viewModel.isDefaultActionButtonDisabled, + action: viewModel.saveOrAddAction + ) ) } ) diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift new file mode 100644 index 0000000000..b8130cb7d6 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift @@ -0,0 +1,148 @@ +// +// AddEditBookmarkFolderDialogView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@MainActor +final class AddEditBookmarkFolderDialogViewModel: ObservableObject { + + enum Mode { + case add + case edit + } + + @Published var folderName: String + @Published var folders: [FolderViewModel] = [] + @Published var selectedFolder: BookmarkFolder? + + let title: String = "Test Title" + let defaultActionTitle = "Test Action Title" + var isDefaultActionButtonDisabled: Bool { false } + + private let mode: Mode + + init(mode: Mode, folderName: String) { + self.mode = mode + self.folderName = folderName + } + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + func addFolderButtonAction() {} +} + +struct AddEditBookmarkFolderDialogView: ModalView { + @ObservedObject private var viewModel: AddEditBookmarkFolderDialogViewModel + + init(viewModel: AddEditBookmarkFolderDialogViewModel) { + self.viewModel = viewModel + } + + var body: some View { + BookmarkDialogContainerView( + title: viewModel.title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $viewModel.folderName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: viewModel.folders, + selectedFolder: $viewModel.selectedFolder, + onActionButton: viewModel.addFolderButtonAction + ) + ) + ) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: UserText.cancel, + keyboardShortCut: .cancelAction, + action: viewModel.cancel + ), defaultButtonAction: .init( + title: viewModel.defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: viewModel.isDefaultActionButtonDisabled, + action: viewModel.addOrSave + ) + ) + } + ) + .font(.system(size: 13)) + .frame(width: 448, height: 210) + } +} + +// MARK: - AddEditBookmarkFolderDialogViewModel.Mode + +extension AddEditBookmarkFolderDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addFolder + case .edit: + return UserText.Bookmarks.Dialog.Title.editFolder + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addFolder + case .edit: + return UserText.save + } + } + +} + +// MARK: - Previews + +#Preview("Add Folder - Light") { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add, folderName: "") + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + .preferredColorScheme(.light) +} + +#Preview("Edit Folder - Light") { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit, folderName: "Test Bookmarks") + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + .preferredColorScheme(.light) +} + +#Preview("Add Folder - Dark") { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add, folderName: "") + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + .preferredColorScheme(.dark) +} + +#Preview("Edit Folder - Dark") { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit, folderName: "Test Bookmarks") + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + .preferredColorScheme(.dark) +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift index 26c9c7284e..7726516704 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift @@ -22,19 +22,16 @@ struct BookmarkDialogButtonsView: View { private let viewState: ViewState private let otherButtonAction: Action private let defaultButtonAction: Action - private let shouldDisableDefaultButtonAction: Bool @Environment(\.dismiss) private var dismiss init( viewState: ViewState, otherButtonAction: Action, - defaultButtonAction: Action, - shouldDisableDefaultButtonAction: Bool + defaultButtonAction: Action ) { self.viewState = viewState self.otherButtonAction = otherButtonAction self.defaultButtonAction = defaultButtonAction - self.shouldDisableDefaultButtonAction = shouldDisableDefaultButtonAction } var body: some View { @@ -46,7 +43,6 @@ struct BookmarkDialogButtonsView: View { actionButton(action: otherButtonAction, viewState: viewState) actionButton(action: defaultButtonAction, viewState: viewState) - .disabled(shouldDisableDefaultButtonAction) } } @@ -60,6 +56,10 @@ struct BookmarkDialogButtonsView: View { .frame(maxWidth: viewState.maxWidth) } .keyboardShortcut(action.keyboardShortCut) + .disabled(action.isDisabled) + .ifLet(action.accessibilityIdentifier) { view, value in + view.accessibilityIdentifier(value) + } } } @@ -74,13 +74,23 @@ extension BookmarkDialogButtonsView { struct Action { let title: String - let action: @MainActor (_ dismiss: () -> Void) -> Void let keyboardShortCut: KeyboardShortcut? + let accessibilityIdentifier: String? + let isDisabled: Bool + let action: @MainActor (_ dismiss: () -> Void) -> Void - init(title: String, action: @MainActor @escaping (_ dismiss: () -> Void) -> Void, keyboardShortCut: KeyboardShortcut? = nil) { + init( + title: String, + accessibilityIdentifier: String? = nil, + keyboardShortCut: KeyboardShortcut? = nil, + isDisabled: Bool = false, + action: @MainActor @escaping (_ dismiss: () -> Void) -> Void + ) { self.title = title - self.action = action self.keyboardShortCut = keyboardShortCut + self.accessibilityIdentifier = accessibilityIdentifier + self.isDisabled = isDisabled + self.action = action } } } @@ -120,9 +130,9 @@ private extension BookmarkDialogButtonsView.ViewState { ), defaultButtonAction: .init( title: "Right", + isDisabled: true, action: {_ in } - ), - shouldDisableDefaultButtonAction: true + ) ) .frame(width: 320, height: 50) } @@ -136,9 +146,9 @@ private extension BookmarkDialogButtonsView.ViewState { ), defaultButtonAction: .init( title: "Right", + isDisabled: false, action: {_ in } - ), - shouldDisableDefaultButtonAction: false + ) ) .frame(width: 320, height: 50) } @@ -152,9 +162,9 @@ private extension BookmarkDialogButtonsView.ViewState { ), defaultButtonAction: .init( title: "Right", + isDisabled: true, action: {_ in } - ), - shouldDisableDefaultButtonAction: true + ) ) .frame(width: 320, height: 50) } @@ -168,9 +178,9 @@ private extension BookmarkDialogButtonsView.ViewState { ), defaultButtonAction: .init( title: "Right", + isDisabled: false, action: {_ in } - ), - shouldDisableDefaultButtonAction: false + ) ) .frame(width: 320, height: 50) } diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift index b474e148b3..88168901ed 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkPopoverViewModel.swift @@ -38,8 +38,24 @@ final class AddBookmarkPopoverViewModel: ObservableObject { } } + @Published var isBookmarkFavorite: Bool { + didSet { + bookmark.isFavorite = isBookmarkFavorite + bookmarkManager.update(bookmark: bookmark) + } + } + + @Published var bookmarkTitle: String { + didSet { + bookmark.title = bookmarkTitle.trimmingWhitespace() + bookmarkManager.update(bookmark: bookmark) + } + } + @Published var addFolderViewModel: AddBookmarkFolderPopoverViewModel? + let isDefaultActionButtonDisabled: Bool = false + private var bookmarkListCancellable: AnyCancellable? init(bookmark: Bookmark, @@ -47,6 +63,7 @@ final class AddBookmarkPopoverViewModel: ObservableObject { self.bookmarkManager = bookmarkManager self.bookmark = bookmark self.bookmarkTitle = bookmark.title + self.isBookmarkFavorite = bookmark.isFavorite bookmarkListCancellable = bookmarkManager.listPublisher .receive(on: DispatchQueue.main) @@ -74,12 +91,6 @@ final class AddBookmarkPopoverViewModel: ObservableObject { dismiss() } - func favoritesButtonAction() { - bookmark.isFavorite.toggle() - - bookmarkManager.update(bookmark: bookmark) - } - func addFolderButtonAction() { addFolderViewModel = .init(bookmark: bookmark, bookmarkManager: bookmarkManager) { [bookmark, bookmarkManager, weak self] newFolder in if let newFolder { @@ -98,14 +109,6 @@ final class AddBookmarkPopoverViewModel: ObservableObject { addFolderViewModel = nil } - @Published var bookmarkTitle: String { - didSet { - bookmark.title = bookmarkTitle.trimmingWhitespace() - - bookmarkManager.update(bookmark: bookmark) - } - } - } struct FolderViewModel: Identifiable, Equatable { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 8fad1dff04..7187ef52d3 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -426,7 +426,6 @@ struct UserText { static let addToFavorites = NSLocalizedString("add.to.favorites", value: "Add to Favorites", comment: "Button for adding bookmarks to favorites") static let addFavorite = NSLocalizedString("add.favorite", value: "Add Favorite", comment: "Button for adding a favorite bookmark") static let editFavorite = NSLocalizedString("edit.favorite", value: "Edit Favorite", comment: "Header of the view that edits a favorite bookmark") - static let editFolder = NSLocalizedString("edit.folder", value: "Edit Folder", comment: "Header of the view that edits a bookmark folder") static let removeFromFavorites = NSLocalizedString("remove.from.favorites", value: "Remove from Favorites", comment: "Button for removing bookmarks from favorites") static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page", comment: "Menu item for bookmarking current page") static let bookmarksShowToolbarPanel = NSLocalizedString("bookmarks.show-toolbar-panel", value: "Open Bookmarks Panel", comment: "Menu item for opening the bookmarks panel") @@ -459,7 +458,6 @@ struct UserText { static let newFolder = NSLocalizedString("folder.optionsMenu.newFolder", value: "New Folder", comment: "Option for creating a new folder") static let renameFolder = NSLocalizedString("folder.optionsMenu.renameFolder", value: "Rename Folder", comment: "Option for renaming a folder") static let deleteFolder = NSLocalizedString("folder.optionsMenu.deleteFolder", value: "Delete Folder", comment: "Option for deleting a folder") - static let newFolderDialogFolderNameTitle = NSLocalizedString("add.folder.name", value: "Name:", comment: "Add Folder popover: folder name text field title") static let newBookmarkDialogBookmarkNameTitle = NSLocalizedString("add.bookmark.name", value: "Name:", comment: "New bookmark folder dialog folder name field heading") static let updateBookmark = NSLocalizedString("bookmark.update", value: "Update Bookmark", comment: "Option for updating a bookmark") diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift index 01b7b3f4ec..15e6bcdc66 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift @@ -286,19 +286,4 @@ private struct VPNLocationViewButtons: View { } -extension View { - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } -} - #endif diff --git a/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift new file mode 100644 index 0000000000..af223c4439 --- /dev/null +++ b/LocalPackages/SwiftUIExtensions/Sources/SwiftUIExtensions/View+ConditionalModifiers.swift @@ -0,0 +1,47 @@ +// +// View+ConditionalModifiers.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +public extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } + + /// Applies the given transform if the given optional value is not `nil`. + /// - Parameters: + /// - value: The optional value to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the optional value is not `nil`. + @ViewBuilder func `ifLet`(_ value: Value?, transform: (Self, Value) -> Content) -> some View { + if let value = value { + transform(self, value) + } else { + self + } + } +} From 8cbf3a419cc647665ea953584830200a478f4609 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 23 Feb 2024 12:00:07 +1100 Subject: [PATCH 03/19] Bookmarks folder add/edit logic (#2221) Task/Issue URL: https://app.asana.com/0/72649045549333/1206590790808721/f **Description**: 1. Add logic and test for `AddEditBookmarkFolderViewModel`. 2. Make a common view to re-use by `AddBookmarkPopoverView` & `AddEditBookmarkDialogView` 3. Enhance `BookmarkManager` & `BookmarkStore` to update Folder title and location in one save. --- DuckDuckGo.xcodeproj/project.pbxproj | 30 ++ .../Bookmarks/Model/BookmarkManager.swift | 7 + .../Bookmarks/Services/BookmarkStore.swift | 1 + .../Services/BookmarkStoreMock.swift | 30 ++ .../Services/LocalBookmarkStore.swift | 94 ++-- .../View/AddBookmarkFolderPopoverView.swift | 51 +-- .../AddEditBookmarkFolderDialogView.swift | 152 +++---- .../Dialog/AddEditBookmarkFolderView.swift | 132 ++++++ .../AddBookmarkFolderPopoverViewModel.swift | 23 +- ...AddEditBookmarkFolderDialogViewModel.swift | 179 ++++++++ .../Model/LocalBookmarkManagerTests.swift | 21 + .../Services/LocalBookmarkStoreTests.swift | 136 ++++++ ...itBookmarkFolderDialogViewModelTests.swift | 426 ++++++++++++++++++ .../HomePage/Mocks/MockBookmarkManager.swift | 2 + 14 files changed, 1112 insertions(+), 172 deletions(-) create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4b386fcdfb..ef11a5d5be 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2420,6 +2420,14 @@ 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; 9F514F932B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */; }; + 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAB2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; + 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; + 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; @@ -4014,6 +4022,9 @@ 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionTests.swift; sourceTree = ""; }; 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtensionTests.swift; sourceTree = ""; }; 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogView.swift; sourceTree = ""; }; + 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; + 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; + 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogButtonsView.swift; sourceTree = ""; }; 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; @@ -6676,6 +6687,14 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F982F102B82264400231028 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; 9FA173DD2B7A0ECE00EE4E6E /* Dialog */ = { isa = PBXGroup; children = ( @@ -6686,6 +6705,7 @@ 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */, 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */, 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */, + 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, ); path = Dialog; sourceTree = ""; @@ -7037,6 +7057,7 @@ AA652CAB25DD820D009059CC /* Bookmarks */ = { isa = PBXGroup; children = ( + 9F982F102B82264400231028 /* ViewModels */, AA652CAE25DD8228009059CC /* Model */, AA652CAF25DD822C009059CC /* Services */, ); @@ -7445,6 +7466,7 @@ AAB549DE25DAB8F80058460B /* BookmarkViewModel.swift */, B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */, B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */, + 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -9772,6 +9794,7 @@ 4B41EDA82B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, 3706FA7B293F65D500E42796 /* FaviconUserScript.swift in Sources */, 3706FA7E293F65D500E42796 /* LottieAnimationCache.swift in Sources */, + 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, 3706FA80293F65D500E42796 /* TabLazyLoaderDataSource.swift in Sources */, 3706FA81293F65D500E42796 /* LoginImport.swift in Sources */, @@ -10071,6 +10094,7 @@ 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, 3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */, 3706FB76293F65D500E42796 /* ASN1Parser.swift in Sources */, + 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */, 987799F42999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, 3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */, @@ -10715,6 +10739,7 @@ 3706FE7B293F661700E42796 /* HistoryStoringMock.swift in Sources */, 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */, 3706FE7C293F661700E42796 /* LocalBookmarkStoreTests.swift in Sources */, + 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */, B6CA4825298CE4B70067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */, 3707C72D294B5D4100682A9F /* EmptyAttributionRulesProver.swift in Sources */, 376E2D2629428353001CD31B /* PrivacyReferenceTestHelper.swift in Sources */, @@ -11185,6 +11210,7 @@ 4B957A202AC7AE700062CA31 /* CancellableExtension.swift in Sources */, 4B957A212AC7AE700062CA31 /* PinnedTabsHostingView.swift in Sources */, 4B957A222AC7AE700062CA31 /* FirefoxBookmarksReader.swift in Sources */, + 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 4B0526622B1D55320054955A /* VPNFeedbackSender.swift in Sources */, 4B957A232AC7AE700062CA31 /* DeviceIdleStateDetector.swift in Sources */, 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */, @@ -11624,6 +11650,7 @@ 4B957BA12AC7AE700062CA31 /* UserDefaults+NetworkProtectionShared.swift in Sources */, 4B957BA22AC7AE700062CA31 /* NavigationActionPolicyExtension.swift in Sources */, 4B957BA32AC7AE700062CA31 /* CIImageExtension.swift in Sources */, + 9F56CFAB2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */, 4B957BA42AC7AE700062CA31 /* NSMenuExtension.swift in Sources */, 4B957BA52AC7AE700062CA31 /* MainWindowController.swift in Sources */, @@ -11873,6 +11900,7 @@ 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */, B62B48562ADE730D000DECE5 /* FileImportView.swift in Sources */, AAA0CC3C25337FAB0079BC96 /* BackForwardListItemViewModel.swift in Sources */, + 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, 1D43EB3429297D760065E5D6 /* BWNotRespondingAlert.swift in Sources */, 4BB88B4525B7B55C006F6B06 /* DebugUserScript.swift in Sources */, AAC6881928626BF800D54247 /* RecentlyClosedTab.swift in Sources */, @@ -12363,6 +12391,7 @@ AAC5E4E425D6BA9C007F5990 /* NSSizeExtension.swift in Sources */, AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, + 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, C1E961EF2B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */, B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, @@ -12498,6 +12527,7 @@ B6619F062B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */, B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */, + 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */, B630E7FE29C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, 4B9292BB2667103100AD2C21 /* BookmarkNodeTests.swift in Sources */, 4B0219A825E0646500ED7DEA /* WebsiteDataStoreTests.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index ee19960da1..7e249c3b48 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -36,6 +36,7 @@ protocol BookmarkManager: AnyObject { func remove(objectsWithUUIDs uuids: [String]) func update(bookmark: Bookmark) func update(folder: BookmarkFolder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) @discardableResult func updateUrl(of bookmark: Bookmark, to newUrl: URL) -> Bookmark? func add(bookmark: Bookmark, to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) func add(objectsWithUUIDs uuids: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) @@ -217,6 +218,12 @@ final class LocalBookmarkManager: BookmarkManager { requestSync() } + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + bookmarkStore.update(folder: folder, andMoveToParent: parent) + loadBookmarks() + requestSync() + } + func updateUrl(of bookmark: Bookmark, to newUrl: URL) -> Bookmark? { guard list != nil else { return nil } guard getBookmark(forUrl: bookmark.url) != nil else { diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index 6b7981c96a..3466d1a7fa 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -52,6 +52,7 @@ protocol BookmarkStore { func remove(objectsWithUUIDs: [String], completion: @escaping (Bool, Error?) -> Void) func update(bookmark: Bookmark) func update(folder: BookmarkFolder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) func add(objectsWithUUIDs: [String], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) func update(objectsWithUUIDs uuids: [String], update: @escaping (BaseBookmarkEntity) -> Void, completion: @escaping (Error?) -> Void) func canMoveObjectWithUUID(objectUUID uuid: String, to parent: BookmarkFolder) -> Bool diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift index f505891891..d098e434e8 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift @@ -46,6 +46,10 @@ public final class BookmarkStoreMock: BookmarkStore { self.updateFavoriteIndexCalled = updateFavoriteIndexCalled } + var capturedFolder: BookmarkFolder? + var capturedParentFolder: BookmarkFolder? + var capturedParentFolderType: ParentFolderType? + var loadAllCalled = false var bookmarks: [BaseBookmarkEntity]? var loadError: Error? @@ -68,6 +72,8 @@ public final class BookmarkStoreMock: BookmarkStore { var saveFolderError: Error? func save(folder: BookmarkFolder, parent: BookmarkFolder?, completion: @escaping (Bool, Error?) -> Void) { saveFolderCalled = true + capturedFolder = folder + capturedParentFolder = parent completion(saveFolderSuccess, saveFolderError) } @@ -97,6 +103,14 @@ public final class BookmarkStoreMock: BookmarkStore { var updateFolderCalled = false func update(folder: BookmarkFolder) { updateFolderCalled = true + capturedFolder = folder + } + + var updateFolderAndMoveToParentCalled = false + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + updateFolderAndMoveToParentCalled = true + capturedFolder = folder + capturedParentFolderType = parent } var addChildCalled = false @@ -122,8 +136,11 @@ public final class BookmarkStoreMock: BookmarkStore { } var moveObjectUUIDCalled = false + var capturedObjectUUIDs: [String]? func move(objectUUIDs: [String], toIndex: Int?, withinParentFolder: ParentFolderType, completion: @escaping (Error?) -> Void) { moveObjectUUIDCalled = true + capturedObjectUUIDs = objectUUIDs + capturedParentFolderType = withinParentFolder } var updateFavoriteIndexCalled = false @@ -135,4 +152,17 @@ public final class BookmarkStoreMock: BookmarkStore { func handleFavoritesAfterDisablingSync() {} } +extension ParentFolderType: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.root, .root): + return true + case (.parent(let lhsValue), .parent(let rhsValue)): + return lhsValue == rhsValue + default: + return false + } + } +} + #endif diff --git a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift index 307d16cc5d..166eb51c02 100644 --- a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift @@ -391,18 +391,21 @@ final class LocalBookmarkStore: BookmarkStore { } func update(folder: BookmarkFolder) { - do { _ = try applyChangesAndSave(changes: { context in - let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: folder.id) - let folderFetchRequestResults = try? context.fetch(folderFetchRequest) - - guard let bookmarkFolderMO = folderFetchRequestResults?.first else { - assertionFailure("LocalBookmarkStore: Failed to get BookmarkEntity from the context") - throw BookmarkStoreError.missingEntity - } + try update(folder: folder, in: context) + }) + } catch { + let error = error as NSError + commonOnSaveErrorHandler(error) + } + } - bookmarkFolderMO.update(with: folder) + func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { + do { + _ = try applyChangesAndSave(changes: { context in + let folderEntity = try update(folder: folder, in: context) + try move(entities: [folderEntity], toIndex: nil, withinParentFolderType: parent, in: context) }) } catch { let error = error as NSError @@ -566,10 +569,6 @@ final class LocalBookmarkStore: BookmarkStore { throw BookmarkStoreError.storeDeallocated } - guard let rootFolder = self.bookmarksRoot(in: context) else { - throw BookmarkStoreError.missingRoot - } - // Guarantee that bookmarks are fetched in the same order as the UUIDs. In the future, this should fetch all objects at once with a // batch fetch request and have them sorted in the correct order. let bookmarkManagedObjects: [BookmarkEntity] = objectUUIDs.compactMap { uuid in @@ -577,28 +576,8 @@ final class LocalBookmarkStore: BookmarkStore { return (try? context.fetch(entityFetchRequest))?.first } - let newParentFolder: BookmarkEntity - - switch type { - case .root: newParentFolder = rootFolder - case .parent(let newParentUUID): - let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) - - if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { - newParentFolder = fetchedParent - } else { - throw BookmarkStoreError.missingEntity - } - } + try move(entities: bookmarkManagedObjects, toIndex: index, withinParentFolderType: type, in: context) - if let index = index, index < newParentFolder.childrenArray.count { - self.move(entities: bookmarkManagedObjects, to: index, within: newParentFolder) - } else { - for bookmarkManagedObject in bookmarkManagedObjects { - bookmarkManagedObject.parent = nil - newParentFolder.addToChildren(bookmarkManagedObject) - } - } }, onError: { [weak self] error in self?.commonOnSaveErrorHandler(error) DispatchQueue.main.async { completion(error) } @@ -996,6 +975,53 @@ final class LocalBookmarkStore: BookmarkStore { } +private extension LocalBookmarkStore { + + @discardableResult + func update(folder: BookmarkFolder, in context: NSManagedObjectContext) throws -> BookmarkEntity { + let folderFetchRequest = BaseBookmarkEntity.singleEntity(with: folder.id) + let folderFetchRequestResults = try? context.fetch(folderFetchRequest) + + guard let bookmarkFolderMO = folderFetchRequestResults?.first else { + assertionFailure("LocalBookmarkStore: Failed to get BookmarkEntity from the context") + throw BookmarkStoreError.missingEntity + } + + bookmarkFolderMO.update(with: folder) + return bookmarkFolderMO + } + + func move(entities: [BookmarkEntity], toIndex index: Int?, withinParentFolderType type: ParentFolderType, in context: NSManagedObjectContext) throws { + guard let rootFolder = bookmarksRoot(in: context) else { + throw BookmarkStoreError.missingRoot + } + + let newParentFolder: BookmarkEntity + + switch type { + case .root: newParentFolder = rootFolder + case .parent(let newParentUUID): + let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) + + if let fetchedParent = try context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { + newParentFolder = fetchedParent + } else { + throw BookmarkStoreError.missingEntity + } + } + + if let index = index, index < newParentFolder.childrenArray.count { + self.move(entities: entities, to: index, within: newParentFolder) + } else { + for bookmarkManagedObject in entities { + bookmarkManagedObject.parent = nil + newParentFolder.addToChildren(bookmarkManagedObject) + } + } + } + +} + extension LocalBookmarkStore.BookmarkStoreError: CustomNSError { var errorCode: Int { diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift index c0c3ae17d3..c283331ced 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift @@ -23,45 +23,18 @@ struct AddBookmarkFolderPopoverView: ModalView { @ObservedObject var model: AddBookmarkFolderPopoverViewModel var body: some View { - BookmarkDialogContainerView( - title: UserText.Bookmarks.Dialog.Title.addFolder, - middleSection: { - BookmarkDialogStackedContentView( - .init( - title: UserText.Bookmarks.Dialog.Field.name, - content: TextField("", text: $model.folderName) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.folder.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - .disabled(model.isDisabled) - ), - .init( - title: UserText.Bookmarks.Dialog.Field.location, - content: BookmarkFolderPicker(folders: model.folders, selectedFolder: $model.parent) - .accessibilityIdentifier("bookmark.folder.folder.dropdown") - .disabled(model.isDisabled) - ) - ) - }, - bottomSection: { - BookmarkDialogButtonsView( - viewState: .expanded, - otherButtonAction: .init( - title: UserText.cancel, - accessibilityIdentifier: "bookmark.add.cancel.button", - isDisabled: model.isDisabled, - action: { _ in model.cancel() } - ), - defaultButtonAction: .init( - title: UserText.Bookmarks.Dialog.Action.addFolder, - accessibilityIdentifier: "bookmark.add.add.folder.button", - keyboardShortCut: .defaultAction, - isDisabled: model.isAddFolderButtonDisabled || model.isDisabled, - action: { _ in model.addFolder() } - ) - ) - } + AddEditBookmarkFolderView( + title: model.title, + buttonsState: .expanded, + folders: model.folders, + folderName: $model.folderName, + selectedFolder: $model.parent, + cancelActionTitle: model.cancelActionTitle, + isCancelActionDisabled: model.isCancelActionDisabled, + cancelAction: { _ in model.cancel() }, + defaultActionTitle: model.defaultActionTitle, + isDefaultActionDisabled: model.isDefaultActionButtonDisabled, + defaultAction: { _ in model.addFolder() } ) .padding(.vertical, 16.0) .font(.system(size: 13)) diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift index b8130cb7d6..ff8f9b827d 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift @@ -18,34 +18,6 @@ import SwiftUI -@MainActor -final class AddEditBookmarkFolderDialogViewModel: ObservableObject { - - enum Mode { - case add - case edit - } - - @Published var folderName: String - @Published var folders: [FolderViewModel] = [] - @Published var selectedFolder: BookmarkFolder? - - let title: String = "Test Title" - let defaultActionTitle = "Test Action Title" - var isDefaultActionButtonDisabled: Bool { false } - - private let mode: Mode - - init(mode: Mode, folderName: String) { - self.mode = mode - self.folderName = folderName - } - - func cancel(dismiss: () -> Void) {} - func addOrSave(dismiss: () -> Void) {} - func addFolderButtonAction() {} -} - struct AddEditBookmarkFolderDialogView: ModalView { @ObservedObject private var viewModel: AddEditBookmarkFolderDialogViewModel @@ -54,95 +26,85 @@ struct AddEditBookmarkFolderDialogView: ModalView { } var body: some View { - BookmarkDialogContainerView( + AddEditBookmarkFolderView( title: viewModel.title, - middleSection: { - BookmarkDialogStackedContentView( - .init( - title: UserText.Bookmarks.Dialog.Field.name, - content: TextField("", text: $viewModel.folderName) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.add.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - ), - .init( - title: UserText.Bookmarks.Dialog.Field.location, - content: BookmarkDialogFolderManagementView( - folders: viewModel.folders, - selectedFolder: $viewModel.selectedFolder, - onActionButton: viewModel.addFolderButtonAction - ) - ) - ) - }, - bottomSection: { - BookmarkDialogButtonsView( - viewState: .compressed, - otherButtonAction: .init( - title: UserText.cancel, - keyboardShortCut: .cancelAction, - action: viewModel.cancel - ), defaultButtonAction: .init( - title: viewModel.defaultActionTitle, - keyboardShortCut: .defaultAction, - isDisabled: viewModel.isDefaultActionButtonDisabled, - action: viewModel.addOrSave - ) - ) - } + buttonsState: .compressed, + folders: viewModel.folders, + folderName: $viewModel.folderName, + selectedFolder: $viewModel.selectedFolder, + cancelActionTitle: viewModel.cancelActionTitle, + isCancelActionDisabled: viewModel.isCancelActionDisabled, + cancelAction: viewModel.cancel, + defaultActionTitle: viewModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.isDefaultActionButtonDisabled, + defaultAction: viewModel.addOrSave ) .font(.system(size: 13)) .frame(width: 448, height: 210) } } -// MARK: - AddEditBookmarkFolderDialogViewModel.Mode - -extension AddEditBookmarkFolderDialogViewModel.Mode { - - var title: String { - switch self { - case .add: - return UserText.Bookmarks.Dialog.Title.addFolder - case .edit: - return UserText.Bookmarks.Dialog.Title.editFolder - } - } - - var defaultActionTitle: String { - switch self { - case .add: - return UserText.Bookmarks.Dialog.Action.addFolder - case .edit: - return UserText.save - } - } - -} - // MARK: - Previews +#if DEBUG +#Preview("Add Folder To Bookmarks - Light") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + .preferredColorScheme(.light) +} -#Preview("Add Folder - Light") { - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add, folderName: "") +#Preview("Add Folder To Bookmarks Subfolder - Light") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: bookmarkFolder), bookmarkManager: bookmarkManager) return AddEditBookmarkFolderDialogView(viewModel: viewModel) .preferredColorScheme(.light) } #Preview("Edit Folder - Light") { - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit, folderName: "Test Bookmarks") + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: bookmarkFolder, parentFolder: nil), bookmarkManager: bookmarkManager) return AddEditBookmarkFolderDialogView(viewModel: viewModel) .preferredColorScheme(.light) } -#Preview("Add Folder - Dark") { - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add, folderName: "") +#Preview("Add Folder To Bookmarks - Dark") { + let store = BookmarkStoreMock(bookmarks: []) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + customAssertionFailure = { _, _, _ in } + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + .preferredColorScheme(.dark) +} + +#Preview("Add Folder To Bookmarks Subfolder - Dark") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + customAssertionFailure = { _, _, _ in } + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: bookmarkFolder), bookmarkManager: bookmarkManager) return AddEditBookmarkFolderDialogView(viewModel: viewModel) .preferredColorScheme(.dark) } -#Preview("Edit Folder - Dark") { - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit, folderName: "Test Bookmarks") +#Preview("Edit Folder in Subfolder - Dark") { + let bookmarkFolder = BookmarkFolder(id: "1", title: "DuckDuckGo", children: []) + let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) + bookmarkManager.loadBookmarks() + customAssertionFailure = { _, _, _ in } + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: bookmarkFolder, parentFolder: bookmarkFolder), bookmarkManager: bookmarkManager) return AddEditBookmarkFolderDialogView(viewModel: viewModel) .preferredColorScheme(.dark) } +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift new file mode 100644 index 0000000000..40c4b19cc0 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift @@ -0,0 +1,132 @@ +// +// AddEditBookmarkFolderView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkFolderView: View { + enum ButtonsState { + case compressed + case expanded + } + + let title: String + let buttonsState: ButtonsState + let folders: [FolderViewModel] + @Binding var folderName: String + @Binding var selectedFolder: BookmarkFolder? + + let cancelActionTitle: String + let isCancelActionDisabled: Bool + let cancelAction: @MainActor (_ dismiss: () -> Void) -> Void + + let defaultActionTitle: String + let isDefaultActionDisabled: Bool + let defaultAction: @MainActor (_ dismiss: () -> Void) -> Void + + var body: some View { + BookmarkDialogContainerView( + title: title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $folderName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkFolderPicker( + folders: folders, + selectedFolder: $selectedFolder + ) + .accessibilityIdentifier("bookmark.folder.folder.dropdown") + ) + ) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .compressed, + otherButtonAction: .init( + title: cancelActionTitle, + keyboardShortCut: .cancelAction, + isDisabled: isCancelActionDisabled, + action: cancelAction + ), defaultButtonAction: .init( + title: defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: isDefaultActionDisabled, + action: defaultAction + ) + ) + } + ) + } +} + +private extension BookmarkDialogButtonsView.ViewState { + + init(_ state: AddEditBookmarkFolderView.ButtonsState) { + switch state { + case .compressed: + self = .compressed + case .expanded: + self = .expanded + } + } +} + +#Preview("Compressed") { + @State var folderName = "" + @State var selectedFolder: BookmarkFolder? + + return AddEditBookmarkFolderView( + title: "Test Title", + buttonsState: .compressed, + folders: [], + folderName: $folderName, + selectedFolder: $selectedFolder, + cancelActionTitle: UserText.cancel, + isCancelActionDisabled: false, + cancelAction: { _ in }, + defaultActionTitle: UserText.save, + isDefaultActionDisabled: false, + defaultAction: { _ in } + ) +} + +#Preview("Expanded") { + @State var folderName = "" + @State var selectedFolder: BookmarkFolder? + + return AddEditBookmarkFolderView( + title: "Test Title", + buttonsState: .expanded, + folders: [], + folderName: $folderName, + selectedFolder: $selectedFolder, + cancelActionTitle: UserText.cancel, + isCancelActionDisabled: false, + cancelAction: { _ in }, + defaultActionTitle: UserText.save, + isDefaultActionDisabled: false, + defaultAction: { _ in } + ) +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift index c16625a362..a1359fa61b 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderPopoverViewModel.swift @@ -22,15 +22,30 @@ import Foundation final class AddBookmarkFolderPopoverViewModel: ObservableObject { private let bookmarkManager: BookmarkManager - let folders: [FolderViewModel] - @Published var parent: BookmarkFolder? + private let completionHandler: (BookmarkFolder?) -> Void + @Published var parent: BookmarkFolder? @Published var folderName: String = "" - @Published private(set) var isDisabled = false - private let completionHandler: (BookmarkFolder?) -> Void + var title: String { + UserText.Bookmarks.Dialog.Title.addFolder + } + + var cancelActionTitle: String { + UserText.cancel + } + + var defaultActionTitle: String { + UserText.Bookmarks.Dialog.Action.addFolder + } + + let isCancelActionDisabled = false + + var isDefaultActionButtonDisabled: Bool { + folderName.trimmingWhitespace().isEmpty || isDisabled + } init(bookmark: Bookmark? = nil, folderName: String = "", diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift new file mode 100644 index 0000000000..a1da3c5262 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift @@ -0,0 +1,179 @@ +// +// AddEditBookmarkFolderDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@MainActor +protocol BookmarkFolderDialogViewModel: ObservableObject { + var title: String { get } + var folderName: String { get } + var folders: [FolderViewModel] { get } + var selectedFolder: BookmarkFolder? { get } + + func cancel() + func addOrSave() +} + +@MainActor +final class AddEditBookmarkFolderDialogViewModel: ObservableObject { + + /// The type of operation to perform on a folder + enum Mode { + /// Add a new folder. Folders can have a parent folder but not necessarily. + /// If the users add a folder to a folder whose parent is the root `Bookmarks` folder, then the parent folder is `nil`. + /// If the users add a folder to a folder whose parent is not the root `Bookmarks` folder, then the parent folder is not `nil`. + case add(parentFolder: BookmarkFolder? = nil) + /// Edit an existing folder. Existing folder can have a parent folder but not necessarily. + /// If the users edit a folder whose parent is the root `Bookmarks` folder, then the parent folder is `nil` + /// If the users edit a folder whose parent is not the root `Bookmarks` folder, then the parent folder is not `nil`. + case edit(folder: BookmarkFolder, parentFolder: BookmarkFolder?) + } + + @Published var folderName: String + @Published var selectedFolder: BookmarkFolder? + + let folders: [FolderViewModel] + + var title: String { + mode.title + } + + var cancelActionTitle: String { + mode.cancelActionTitle + } + + var defaultActionTitle: String { + mode.defaultActionTitle + } + + let isCancelActionDisabled = false + + var isDefaultActionButtonDisabled: Bool { + folderName.trimmingWhitespace().isEmpty + } + + private let mode: Mode + private let bookmarkManager: BookmarkManager + + init(mode: Mode, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { + self.mode = mode + self.bookmarkManager = bookmarkManager + folderName = mode.folderName + folders = .init(bookmarkManager.list) + selectedFolder = mode.parentFolder + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + defer { dismiss() } + + guard !folderName.isEmpty else { + assertionFailure("folderName is empty, button should be disabled") + return + } + + let folderName = folderName.trimmingWhitespace() + + switch mode { + case let .edit(folder, originalParent): + // If there are no pending changes dismiss + guard folder.title != folderName || selectedFolder?.id != originalParent?.id else { return } + // Otherwise update Folder. + update(folder: folder, originalParent: originalParent, newParent: selectedFolder) + case .add: + add(folderWithName: folderName, to: selectedFolder) + } + } + +} + +// MARK: - Private + +private extension AddEditBookmarkFolderDialogViewModel { + + func update(folder: BookmarkFolder, originalParent: BookmarkFolder?, newParent: BookmarkFolder?) { + // If the original location of the folder changed move it to the new folder. + if selectedFolder?.id != originalParent?.id { + // Update the title anyway. + folder.title = folderName + let parentFolderType: ParentFolderType = newParent.flatMap { ParentFolderType.parent(uuid: $0.id) } ?? .root + bookmarkManager.update(folder: folder, andMoveToParent: parentFolderType) + } else if folder.title != folderName { // If only title changed just update the folder title without updating its parent. + folder.title = folderName + bookmarkManager.update(folder: folder) + } + } + + func add(folderWithName name: String, to parent: BookmarkFolder?) { + bookmarkManager.makeFolder(for: name, parent: parent, completion: { _ in }) + } + +} + +// MARK: - AddEditBookmarkFolderDialogViewModel.Mode + +private extension AddEditBookmarkFolderDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addFolder + case .edit: + return UserText.Bookmarks.Dialog.Title.editFolder + } + } + + var cancelActionTitle: String { + switch self { + case .add, .edit: + return UserText.cancel + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addFolder + case .edit: + return UserText.save + } + } + + var folderName: String { + switch self { + case .add: + return "" + case let .edit(folder, _): + return folder.title + } + } + + var parentFolder: BookmarkFolder? { + switch self { + case let .add(parentFolder): + return parentFolder + case let .edit(_, parentFolder): + return parentFolder + } + } + +} diff --git a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift index b82636a7c5..96e1c7d4e3 100644 --- a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift +++ b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Combine import Foundation import XCTest @@ -157,6 +158,26 @@ final class LocalBookmarkManagerTests: XCTestCase { XCTAssert(bookmarkStoreMock.updateBookmarkCalled) } + func testWhenBookmarkFolderIsUpdatedAndMoved_ThenManagerUpdatesItAlsoInStore() throws { + let (bookmarkManager, bookmarkStoreMock) = LocalBookmarkManager.aManager + let parent = BookmarkFolder(id: "1", title: "Parent") + let folder = BookmarkFolder(id: "2", title: "Child") + var bookmarkList: BookmarkList? + let cancellable = bookmarkManager.listPublisher + .dropFirst() + .sink { list in + bookmarkList = list + } + + bookmarkManager.update(folder: folder, andMoveToParent: .parent(uuid: parent.id)) + + let topLevelEntities = try XCTUnwrap(bookmarkList?.topLevelEntities) + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: parent.id)) + XCTAssertNotNil(bookmarkList) + } + } fileprivate extension LocalBookmarkManager { diff --git a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift index 0dff288b23..12812e9935 100644 --- a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -468,6 +468,142 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(topLevelEntityIDs, [testState.initialParentFolder.id, testState.bookmark3.id]) } + func testWhenUpdatingBookmarkFolder_ThenBookmarkFolderTitleIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(folders.first, folder1) + + // Update the folder title and parent: + + let folderToMove = folder1 + folderToMove.title = #function + bookmarkStore.update(folder: folder1) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 1) + XCTAssertEqual(newFolders.first, folderToMove) + } + + func testWhenUpdatingAndMovingBookmarkFolder_ThenBookmarkFolderIsMovedAndTitleUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2") + let folder3 = BookmarkFolder(id: UUID().uuidString, title: "Folder 3") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + _ = await bookmarkStore.save(folder: folder2, parent: nil) + _ = await bookmarkStore.save(folder: folder3, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 3) + XCTAssertEqual(folders[0], folder1) + XCTAssertEqual(folders[1], folder2) + XCTAssertEqual(folders[2], folder3) + + // Update the folder title and parent: + + let folderToMove = folder1 + folderToMove.title = #function + bookmarkStore.update(folder: folder1, andMoveToParent: .parent(uuid: folder2.id)) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 2) + XCTAssertEqual(newFolders[0].id, folder2.id) + XCTAssertEqual(newFolders[0].children, [folder1]) + XCTAssertEqual(newFolders[1], folder3) + } + + func testWhenMovingBookmarkFolderToSubfolder_ThenBookmarkFolderLocationIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder1, parent: nil) + _ = await bookmarkStore.save(folder: folder2, parent: nil) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 2) + XCTAssertEqual(folders.first, folder1) + XCTAssertEqual(folders.last, folder2) + + // Update the folder parent: + + _ = await bookmarkStore.move(objectUUIDs: [folder2.id], toIndex: nil, withinParentFolder: .parent(uuid: folder1.id)) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 1) + XCTAssertEqual(newFolders.first, folder1) + XCTAssertEqual(newFolders.first?.children, [folder2]) + } + + func testWhenMovingBookmarkFolderToRootFolder_ThenBookmarkFolderLocationIsUpdated() async throws { + let context = container.viewContext + let bookmarkStore = LocalBookmarkStore(context: context) + + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2") + + // Save the initial bookmarks state: + + _ = await bookmarkStore.save(folder: folder2, parent: nil) + _ = await bookmarkStore.save(folder: folder1, parent: folder2) + + // Fetch persisted bookmark folders back from the store: + + let folders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(folders.count, 1) + XCTAssertEqual(folders.first, folder2) + XCTAssertEqual(folders.first?.children, [folder1]) + + // Update the folder parent: + + _ = await bookmarkStore.move(objectUUIDs: [folder1.id], toIndex: 0, withinParentFolder: .root) + + // Check the new bookmark folders order: + + let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + + XCTAssertEqual(newFolders.count, 2) + XCTAssertEqual(newFolders.first, folder1) + XCTAssertEqual(newFolders.last, folder2) + XCTAssertEqual(newFolders.last?.children, []) + } + // MARK: Favorites func testThatTopLevelEntitiesDoNotContainFavoritesFolder() async { diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift new file mode 100644 index 0000000000..135fc69cbf --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift @@ -0,0 +1,426 @@ +// +// AddEditBookmarkFolderDialogViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testReturnAddBookmarkFolderTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.addFolder) + } + + func testReturnEditBookmarkFolderTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.editFolder) + } + + func testReturnCancelActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnCancelActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnAddBookmarkFolderActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addFolder) + } + + func testReturnSaveActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.save) + } + + // MARK: State + + func testShouldSetFolderNameToEmptyWhenInitAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertTrue(result.isEmpty) + } + + func testShouldSetFolderNameToValueWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folderName + + // THEN + XCTAssertEqual(result, #function) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, .mock) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, .mock) + } + + // MARK: - Actions + + func testReturnIsCancelActionDisabledFalseWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isCancelActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsCancelActionDisabledFalseWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isCancelActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenFolderNameIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.folderName = "" + + // WHEN + let result = sut.isDefaultActionButtonDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenFolderNameIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = "" + + // WHEN + let result = sut.isDefaultActionButtonDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenFolderNameIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.folderName = " Test " + + // WHEN + let result = sut.isDefaultActionButtonDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenFolderNameIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = " Test " + + // WHEN + let result = sut.isDefaultActionButtonDisabled + + // THEN + XCTAssertFalse(result) + } + + func testShouldCallDismissWhenCancelIsCalled() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldCallDismissWhenAddOrSaveIsCalled() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldAskBookmarkStoreToSaveFolderWhenAddOrSaveIsCalledAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.folderName = #function + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertTrue(bookmarkStoreMock.saveFolderCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) + } + + func testShouldAskBookmarkStoreToUpdateFolderWhenNameIsChanged() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.folderName = "TEST" + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertTrue(bookmarkStoreMock.updateFolderCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder?.title, sut.folderName) + } + + func testShouldNotAskBookmarkStoreToUpdateFolderWhenNameIsNotChanged() { + // GIVEN + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertFalse(bookmarkStoreMock.updateFolderCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder?.title) + } + + func testShouldAskBookmarkStoreToMoveFolderToSubfolderWhenSelectedFolderIsDifferentFromOriginalFolder() { + // GIVEN + let location = BookmarkFolder(id: #file, title: #function) + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.selectedFolder = location + XCTAssertFalse(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: #file)) + } + + func testShouldAskBookmarkStoreToMoveFolderToRootFolderWhenSelectedFolderIsDifferentFromOriginalFolder() { + // GIVEN + let location = BookmarkFolder(id: #file, title: #function) + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertNil(bookmarkStoreMock.capturedFolder) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) + XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + } + + func testShouldNotAskBookmarkStoreToMoveFolderWhenSelectedFolderIsNotDifferentFromOriginalFolder() { + // GIVEN + let location = BookmarkFolder(id: #file, title: #function) + let folder = BookmarkFolder.mock + let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveFolderCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } + +} diff --git a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift index c6a0840629..0d719e1a87 100644 --- a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift +++ b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift @@ -61,6 +61,8 @@ class MockBookmarkManager: BookmarkManager { func update(folder: DuckDuckGo_Privacy_Browser.BookmarkFolder) {} + func update(folder: DuckDuckGo_Privacy_Browser.BookmarkFolder, andMoveToParent parent: DuckDuckGo_Privacy_Browser.ParentFolderType) {} + func updateUrl(of bookmark: DuckDuckGo_Privacy_Browser.Bookmark, to newUrl: URL) -> DuckDuckGo_Privacy_Browser.Bookmark? { return nil } From bf47c3037db9a1bcfe0b1b4289b13816639d6390 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 26 Feb 2024 21:11:26 +1100 Subject: [PATCH 04/19] Bookmarks add/edit logic (#2231) Task/Issue URL: https://app.asana.com/0/0/1206590790808718/f **Description**: 1. Implementation of add/edit bookmark dialog logic + tests. 2. Extracted view to be reused by AddBookmarkPopoverView & AddEditBookmarkDialogView --- DuckDuckGo.xcodeproj/project.pbxproj | 44 ++ .../Services/BookmarkStoreMock.swift | 4 + .../View/AddBookmarkPopoverView.swift | 53 +- .../Dialog/AddEditBookmarkDialogView.swift | 201 +++--- .../AddEditBookmarkFolderDialogView.swift | 4 +- .../View/Dialog/AddEditBookmarkView.swift | 113 ++++ .../BookmarkDialogStackedContentView.swift | 16 +- ...itBookmarkDialogCoordinatorViewModel.swift | 74 +++ .../AddEditBookmarkDialogViewModel.swift | 182 ++++++ ...AddEditBookmarkFolderDialogViewModel.swift | 26 +- .../ViewModel/BookmarksDialogViewModel.swift | 35 ++ ...kmarkDialogCoordinatorViewModelTests.swift | 170 +++++ .../AddEditBookmarkDialogViewModelTests.swift | 591 ++++++++++++++++++ ...itBookmarkFolderDialogViewModelTests.swift | 17 +- 14 files changed, 1343 insertions(+), 187 deletions(-) create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift create mode 100644 DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift create mode 100644 UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ef11a5d5be..9657c8b349 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2413,6 +2413,10 @@ 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; 9F180D132B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; + 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; + 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; + 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; + 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; @@ -2423,6 +2427,9 @@ 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; 9F56CFAB2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; @@ -2446,6 +2453,15 @@ 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98672B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; + 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; + 9FEE986F2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = AA06B6B62672AF8100F541C5 /* Sparkle */; }; AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */; }; AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */; }; @@ -4019,10 +4035,13 @@ 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+WKUIDelegateTests.swift"; sourceTree = ""; }; 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; + 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelTests.swift; sourceTree = ""; }; + 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionTests.swift; sourceTree = ""; }; 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtensionTests.swift; sourceTree = ""; }; 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogView.swift; sourceTree = ""; }; 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; + 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; @@ -4031,6 +4050,9 @@ 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFavoriteView.swift; sourceTree = ""; }; + 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkView.swift; sourceTree = ""; }; + 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewModel.swift; sourceTree = ""; }; + 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModel.swift; sourceTree = ""; }; AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariVersionReaderTests.swift; sourceTree = ""; }; AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProviderTests.swift; sourceTree = ""; }; AA0F3DB6261A566C0077F2D9 /* SuggestionLoadingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionLoadingMock.swift; sourceTree = ""; }; @@ -6691,6 +6713,8 @@ isa = PBXGroup; children = ( 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */, + 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */, + 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */, ); path = ViewModels; sourceTree = ""; @@ -6706,6 +6730,7 @@ 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */, 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */, 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, + 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */, ); path = Dialog; sourceTree = ""; @@ -7467,6 +7492,9 @@ B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */, B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */, 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */, + 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */, + 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */, + 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -9810,6 +9838,7 @@ 3706FA89293F65D500E42796 /* CrashReportPromptPresenter.swift in Sources */, 3706FA8B293F65D500E42796 /* PreferencesRootView.swift in Sources */, 3706FA8C293F65D500E42796 /* AppStateChangedPublisher.swift in Sources */, + 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 3706FA8D293F65D500E42796 /* BookmarkTableCellView.swift in Sources */, 3706FA8E293F65D500E42796 /* BookmarkManagementSidebarViewController.swift in Sources */, 3706FA8F293F65D500E42796 /* NSStackViewExtension.swift in Sources */, @@ -10077,6 +10106,7 @@ 3706FB69293F65D500E42796 /* NavigationBarBadgeAnimationView.swift in Sources */, 1D1A334A2A6FEB170080ACED /* BurnerMode.swift in Sources */, B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, + 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, @@ -10243,6 +10273,7 @@ 3706FBD9293F65D500E42796 /* NSAppearanceExtension.swift in Sources */, 3706FBDA293F65D500E42796 /* PermissionManager.swift in Sources */, 3706FBDB293F65D500E42796 /* DefaultBrowserPreferences.swift in Sources */, + 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 3706FBDC293F65D500E42796 /* Permissions.xcdatamodeld in Sources */, 4B41EDB52B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift in Sources */, 3706FBDD293F65D500E42796 /* PaddedImageButton.swift in Sources */, @@ -10279,6 +10310,7 @@ 3706FBF0293F65D500E42796 /* PasswordManagementItemModel.swift in Sources */, 3706FBF2293F65D500E42796 /* FindInPageModel.swift in Sources */, 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, + 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */, 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */, 3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */, @@ -10531,6 +10563,7 @@ 3706FDDE293F661700E42796 /* SuggestionViewModelTests.swift in Sources */, 3706FDDF293F661700E42796 /* BookmarkSidebarTreeControllerTests.swift in Sources */, 3706FDE0293F661700E42796 /* TabIndexTests.swift in Sources */, + 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 3706FDE1293F661700E42796 /* AdjacentItemEnumeratorTests.swift in Sources */, 3706FDE2293F661700E42796 /* PixelArgumentsTests.swift in Sources */, 4B9DB0572A983B55000927DB /* MockNotificationService.swift in Sources */, @@ -10621,6 +10654,7 @@ 3706FE26293F661700E42796 /* TemporaryFileCreator.swift in Sources */, 3706FE27293F661700E42796 /* AppPrivacyConfigurationTests.swift in Sources */, B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, @@ -11020,6 +11054,7 @@ 4B95796E2AC7AE700062CA31 /* LegacyBookmarkStore.swift in Sources */, 4B95796F2AC7AE700062CA31 /* NSAlert+DataImport.swift in Sources */, 4B9579702AC7AE700062CA31 /* MainWindow.swift in Sources */, + 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 4B9579712AC7AE700062CA31 /* CrashReportPromptViewController.swift in Sources */, 4B9579722AC7AE700062CA31 /* BookmarksCleanupErrorHandling.swift in Sources */, 4B9579732AC7AE700062CA31 /* ContextMenuManager.swift in Sources */, @@ -11147,6 +11182,7 @@ 4B9579E42AC7AE700062CA31 /* PopUpWindow.swift in Sources */, 4B9579E52AC7AE700062CA31 /* Favicons.xcdatamodeld in Sources */, 4B9579E62AC7AE700062CA31 /* Publisher.asVoid.swift in Sources */, + 9FEE986F2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 4B9579E72AC7AE700062CA31 /* Waitlist.swift in Sources */, 3158B1582B0BF76000AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, 4B9579E82AC7AE700062CA31 /* NavigationButtonMenuDelegate.swift in Sources */, @@ -11234,6 +11270,7 @@ 4B957A332AC7AE700062CA31 /* Favicon.swift in Sources */, 1E2AE4CA2ACB21A000684E0A /* NetworkProtectionRemoteMessage.swift in Sources */, 4B957A342AC7AE700062CA31 /* SuggestionContainerViewModel.swift in Sources */, + 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 4B957A352AC7AE700062CA31 /* FirePopoverWrapperViewController.swift in Sources */, 4B957A362AC7AE700062CA31 /* NSPasteboardItemExtension.swift in Sources */, 4B957A372AC7AE700062CA31 /* AutofillPreferencesModel.swift in Sources */, @@ -11383,6 +11420,7 @@ 4B957AB62AC7AE700062CA31 /* FireproofDomains.xcdatamodeld in Sources */, 3158B14F2B0BF74F00AF130C /* DataBrokerProtectionManager.swift in Sources */, 4B957AB82AC7AE700062CA31 /* HomePageView.swift in Sources */, + 9FEE98672B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 4B957AB92AC7AE700062CA31 /* SerpHeadersNavigationResponder.swift in Sources */, 4B957ABA2AC7AE700062CA31 /* HomePageContinueSetUpModel.swift in Sources */, 4B957ABB2AC7AE700062CA31 /* WebKitDownloadTask.swift in Sources */, @@ -11998,6 +12036,7 @@ 85C6A29625CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift in Sources */, 85625998269C9C5F00EE44BC /* PasswordManagementPopover.swift in Sources */, 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */, + 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 85589E9127BFB9810038AD11 /* HomePageRecentlyVisitedModel.swift in Sources */, 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */, B626A7602992407D00053070 /* CancellableExtension.swift in Sources */, @@ -12120,6 +12159,7 @@ 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */, 1DB67F292B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift in Sources */, 4B9292CE2667123700AD2C21 /* BrowserTabSelectionDelegate.swift in Sources */, + 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, B6BCC53B2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, @@ -12218,6 +12258,7 @@ 4BE4005527CF3F19007D3161 /* SavePaymentMethodViewController.swift in Sources */, 1D2DC009290167A0008083A1 /* BWStatus.swift in Sources */, AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */, + 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, B63D467A25BFC3E100874977 /* NSCoderExtensions.swift in Sources */, 1D2DC00B290167EC008083A1 /* RunningApplicationCheck.swift in Sources */, B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, @@ -12396,6 +12437,7 @@ C1E961EF2B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */, B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, 4B9292AF26670F5300AD2C21 /* NSOutlineViewExtensions.swift in Sources */, + 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, B687B7CA2947A029001DEA6F /* ContentBlockingTabExtension.swift in Sources */, @@ -12570,6 +12612,7 @@ 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, 8546DE6225C03056000CA5E1 /* UserAgentTests.swift in Sources */, + 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, B63ED0DE26AFD9A300A9DAD1 /* AVCaptureDeviceMock.swift in Sources */, 98A95D88299A2DF900B9B81A /* BookmarkMigrationTests.swift in Sources */, B63ED0E026AFE32F00A9DAD1 /* GeolocationProviderMock.swift in Sources */, @@ -12673,6 +12716,7 @@ 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, B603971029B9D67E00902A34 /* PublishersExtensions.swift in Sources */, + 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 4B9292C32667103100AD2C21 /* PasteboardBookmarkTests.swift in Sources */, B610F2E427A8F37A00FCEBE9 /* CBRCompileTimeReporterTests.swift in Sources */, AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift index d098e434e8..2ab57158a9 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift @@ -49,6 +49,7 @@ public final class BookmarkStoreMock: BookmarkStore { var capturedFolder: BookmarkFolder? var capturedParentFolder: BookmarkFolder? var capturedParentFolderType: ParentFolderType? + var capturedBookmark: Bookmark? var loadAllCalled = false var bookmarks: [BaseBookmarkEntity]? @@ -64,6 +65,8 @@ public final class BookmarkStoreMock: BookmarkStore { func save(bookmark: Bookmark, parent: BookmarkFolder?, index: Int?, completion: @escaping (Bool, Error?) -> Void) { saveBookmarkCalled = true bookmarks?.append(bookmark) + capturedParentFolder = parent + capturedBookmark = bookmark completion(saveBookmarkSuccess, saveBookmarkError) } @@ -98,6 +101,7 @@ public final class BookmarkStoreMock: BookmarkStore { } updateBookmarkCalled = true + capturedBookmark = bookmark } var updateFolderCalled = false diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index 2582b61691..8ddb38ce2c 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -38,44 +38,22 @@ struct AddBookmarkPopoverView: View { @MainActor private var addBookmarkView: some View { - BookmarkDialogContainerView( + AddEditBookmarkView( title: UserText.Bookmarks.Dialog.Title.addedBookmark, - middleSection: { - BookmarkDialogStackedContentView( - .init( - title: UserText.Bookmarks.Dialog.Field.name, - content: TextField("", text: $model.bookmarkTitle) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.add.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - ), - .init( - title: UserText.Bookmarks.Dialog.Field.location, - content: BookmarkDialogFolderManagementView( - folders: model.folders, - selectedFolder: $model.selectedFolder, - onActionButton: model.addFolderButtonAction - ) - ) - ) - BookmarkFavoriteView(isFavorite: $model.isBookmarkFavorite) - }, - bottomSection: { - BookmarkDialogButtonsView( - viewState: .expanded, - otherButtonAction: .init( - title: UserText.remove, - action: model.removeButtonAction - ), - defaultButtonAction: .init( - title: UserText.done, - keyboardShortCut: .defaultAction, - isDisabled: model.isDefaultActionButtonDisabled, - action: model.doneButtonAction - ) - ) - } + buttonsState: .compressed, + bookmarkName: $model.bookmarkTitle, + bookmarkURLPath: nil, + isBookmarkFavorite: $model.isBookmarkFavorite, + folders: model.folders, + selectedFolder: $model.selectedFolder, + isURLFieldHidden: true, + addFolderAction: model.addFolderButtonAction, + otherActionTitle: UserText.remove, + isOtherActionDisabled: false, + otherAction: model.removeButtonAction, + defaultActionTitle: UserText.done, + isDefaultActionDisabled: model.isDefaultActionButtonDisabled, + defaultAction: model.doneButtonAction ) .padding(.vertical, 16.0) .font(.system(size: 13)) @@ -112,6 +90,7 @@ struct AddBookmarkPopoverView: View { let bkm = Bookmark(id: "n", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false, parentFolderUUID: "1") let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ BookmarkFolder(id: "1", title: "Folder with a name that shouldn‘t fit into the picker", children: [])])) + bkman.loadBookmarks() return AddBookmarkPopoverView(model: AddBookmarkPopoverViewModel(bookmark: bkm, bookmarkManager: bkman)) .preferredColorScheme(.dark) diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift index 6d478c5527..103a49ed43 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -17,158 +17,115 @@ // import SwiftUI -import Combine -import Common - -@MainActor -final class AddEditBookmarkDialogViewModel: ObservableObject { - - enum Mode { - case add - case edit - } - - @Published var bookmarkName: String - @Published var bookmarkURLPath: String - @Published var isBookmarkFavorite: Bool - @Published var folders: [FolderViewModel] = [] - @Published var selectedFolder: BookmarkFolder? - - let title: String = "" - let defaultActionTitle = "" - var isDefaultActionButtonDisabled: Bool { false } - - private let bookmark: Bookmark - private let mode: Mode - private let bookmarkManager: BookmarkManager - - init( - mode: Mode, - bookmark: Bookmark, - bookmarkManager: LocalBookmarkManager = .shared - ) { - self.bookmark = bookmark - self.mode = mode - self.bookmarkManager = bookmarkManager - bookmarkName = bookmark.title - bookmarkURLPath = bookmark.url - isBookmarkFavorite = bookmark.isFavorite - } - - func cancelAction(dismiss: () -> Void) {} - func saveOrAddAction(dismiss: () -> Void) {} - func addFolderButtonAction() {} -} struct AddEditBookmarkDialogView: ModalView { - @ObservedObject private var viewModel: AddEditBookmarkDialogViewModel + @ObservedObject private var viewModel: AddEditBookmarkDialogCoordinatorViewModel - init(viewModel: AddEditBookmarkDialogViewModel) { + init(viewModel: AddEditBookmarkDialogCoordinatorViewModel) { self.viewModel = viewModel } var body: some View { - BookmarkDialogContainerView( - title: viewModel.title, - middleSection: { - BookmarkDialogStackedContentView( - .init( - title: UserText.Bookmarks.Dialog.Field.name, - content: TextField("", text: $viewModel.bookmarkName) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.add.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - ), - .init( - title: UserText.Bookmarks.Dialog.Field.url, - content: TextField("", text: $viewModel.bookmarkURLPath) - .accessibilityIdentifier("bookmark.add.url.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - ), - .init( - title: UserText.Bookmarks.Dialog.Field.location, - content: BookmarkDialogFolderManagementView( - folders: viewModel.folders, - selectedFolder: $viewModel.selectedFolder, - onActionButton: viewModel.addFolderButtonAction - ) - ) - ) - BookmarkFavoriteView(isFavorite: $viewModel.isBookmarkFavorite) - }, - bottomSection: { - BookmarkDialogButtonsView( - viewState: .compressed, - otherButtonAction: .init( - title: UserText.cancel, - keyboardShortCut: .cancelAction, - action: viewModel.cancelAction - ), defaultButtonAction: .init( - title: viewModel.defaultActionTitle, - keyboardShortCut: .defaultAction, - isDisabled: viewModel.isDefaultActionButtonDisabled, - action: viewModel.saveOrAddAction - ) - ) + Group { + switch viewModel.viewState { + case .bookmark: + addEditBookmarkView + case .folder: + addFolderView } - ) + } .font(.system(size: 13)) - .frame(width: 448, height: 288) } -} - -// MARK: - AddEditBookmarkDialogViewModel.Mode -private extension AddEditBookmarkDialogViewModel.Mode { - - var title: String { - switch self { - case .add: - return UserText.Bookmarks.Dialog.Title.addBookmark - case .edit: - return UserText.Bookmarks.Dialog.Title.editBookmark - } + private var addEditBookmarkView: some View { + AddEditBookmarkView( + title: viewModel.bookmarkModel.title, + buttonsState: .compressed, + bookmarkName: $viewModel.bookmarkModel.bookmarkName, + bookmarkURLPath: $viewModel.bookmarkModel.bookmarkURLPath, + isBookmarkFavorite: $viewModel.bookmarkModel.isBookmarkFavorite, + folders: viewModel.bookmarkModel.folders, + selectedFolder: $viewModel.bookmarkModel.selectedFolder, + isURLFieldHidden: false, + addFolderAction: viewModel.addFolderAction, + otherActionTitle: viewModel.bookmarkModel.cancelActionTitle, + isOtherActionDisabled: viewModel.bookmarkModel.isOtherActionDisabled, + otherAction: viewModel.bookmarkModel.cancel, + defaultActionTitle: viewModel.bookmarkModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, + defaultAction: viewModel.bookmarkModel.addOrSave + ) + .frame(width: 448, height: 288) } - var defaultActionTitle: String { - switch self { - case .add: - return UserText.Bookmarks.Dialog.Action.addBookmark - case .edit: - return UserText.save - } + private var addFolderView: some View { + AddEditBookmarkFolderView( + title: viewModel.folderModel.title, + buttonsState: .compressed, + folders: viewModel.folderModel.folders, + folderName: $viewModel.folderModel.folderName, + selectedFolder: $viewModel.folderModel.selectedFolder, + cancelActionTitle: viewModel.folderModel.cancelActionTitle, + isCancelActionDisabled: viewModel.folderModel.isOtherActionDisabled, + cancelAction: { _ in + viewModel.dismissAction() + }, + defaultActionTitle: viewModel.folderModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.folderModel.isDefaultActionDisabled, + defaultAction: { _ in + viewModel.folderModel.addOrSave { + viewModel.dismissAction() + } + } + ) + .frame(width: 448, height: 210) } - } // MARK: - Previews +#if DEBUG #Preview("Add Bookmark - Light Mode") { - let bookmark = Bookmark(id: "1", url: "", title: "", isFavorite: false) - let viewModel = AddEditBookmarkDialogViewModel(mode: .add, bookmark: bookmark) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bookmarkManager.loadBookmarks() + let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) return AddEditBookmarkDialogView(viewModel: viewModel) .preferredColorScheme(.light) } -#Preview("Edit Bookmark - Light Mode") { - let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true) - let viewModel = AddEditBookmarkDialogViewModel(mode: .edit, bookmark: bookmark) +#Preview("Add Bookmark - Light Mode") { + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bookmarkManager.loadBookmarks() + let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) return AddEditBookmarkDialogView(viewModel: viewModel) - .preferredColorScheme(.light) + .preferredColorScheme(.dark) } -#Preview("Add Bookmark - Dark Mode") { - let bookmark = Bookmark(id: "1", url: "", title: "", isFavorite: false) - let viewModel = AddEditBookmarkDialogViewModel(mode: .add, bookmark: bookmark) +#Preview("Edit Bookmark - Light Mode") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) return AddEditBookmarkDialogView(viewModel: viewModel) - .preferredColorScheme(.dark) + .preferredColorScheme(.light) } #Preview("Edit Bookmark - Dark Mode") { - let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true) - let viewModel = AddEditBookmarkDialogViewModel(mode: .edit, bookmark: bookmark) + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) return AddEditBookmarkDialogView(viewModel: viewModel) .preferredColorScheme(.dark) } +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift index ff8f9b827d..f1a9174fa9 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift @@ -33,10 +33,10 @@ struct AddEditBookmarkFolderDialogView: ModalView { folderName: $viewModel.folderName, selectedFolder: $viewModel.selectedFolder, cancelActionTitle: viewModel.cancelActionTitle, - isCancelActionDisabled: viewModel.isCancelActionDisabled, + isCancelActionDisabled: viewModel.isOtherActionDisabled, cancelAction: viewModel.cancel, defaultActionTitle: viewModel.defaultActionTitle, - isDefaultActionDisabled: viewModel.isDefaultActionButtonDisabled, + isDefaultActionDisabled: viewModel.isDefaultActionDisabled, defaultAction: viewModel.addOrSave ) .font(.system(size: 13)) diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift new file mode 100644 index 0000000000..8d34889432 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift @@ -0,0 +1,113 @@ +// +// AddEditBookmarkView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct AddEditBookmarkView: View { + let title: String + let buttonsState: BookmarksDialogButtonsState + + @Binding var bookmarkName: String + var bookmarkURLPath: Binding? + @Binding var isBookmarkFavorite: Bool + + let folders: [FolderViewModel] + @Binding var selectedFolder: BookmarkFolder? + + let isURLFieldHidden: Bool + + let addFolderAction: () -> Void + + let otherActionTitle: String + let isOtherActionDisabled: Bool + let otherAction: @MainActor (_ dismiss: () -> Void) -> Void + + let defaultActionTitle: String + let isDefaultActionDisabled: Bool + let defaultAction: @MainActor (_ dismiss: () -> Void) -> Void + + var body: some View { + BookmarkDialogContainerView( + title: title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $bookmarkName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.url, + content: TextField("", text: bookmarkURLPath ?? .constant("")) + .accessibilityIdentifier("bookmark.add.url.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)), + isContentViewHidden: isURLFieldHidden + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: folders, + selectedFolder: $selectedFolder, + onActionButton: addFolderAction + ) + ) + ) + BookmarkFavoriteView(isFavorite: $isBookmarkFavorite) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .init(buttonsState), + otherButtonAction: .init( + title: otherActionTitle, + isDisabled: isOtherActionDisabled, + action: otherAction + ), + defaultButtonAction: .init( + title: defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: isDefaultActionDisabled, + action: defaultAction + ) + ) + } + ) + } + +} + +// MARK: - BookmarksDialogButtonsState + +enum BookmarksDialogButtonsState { + case compressed + case expanded +} + +extension BookmarkDialogButtonsView.ViewState { + init(_ state: BookmarksDialogButtonsState) { + switch state { + case .compressed: + self = .compressed + case .expanded: + self = .expanded + } + } +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift index df63a94090..864b0cdb13 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift @@ -37,14 +37,18 @@ struct BookmarkDialogStackedContentView: View { rowHeight: 22.0, leftColumn: { ForEach(items, id: \.title) { item in - Text(item.title) - .foregroundColor(.primary) - .fontWeight(.medium) + if !item.isContentViewHidden { + Text(item.title) + .foregroundColor(.primary) + .fontWeight(.medium) + } } }, rightColumn: { ForEach(items, id: \.title) { item in - item.content + if !item.isContentViewHidden { + item.content + } } } ) @@ -57,10 +61,12 @@ extension BookmarkDialogStackedContentView { struct Item { fileprivate let title: String fileprivate let content: AnyView + fileprivate let isContentViewHidden: Bool - init(title: String, content: any View) { + init(title: String, content: any View, isContentViewHidden: Bool = false) { self.title = title self.content = AnyView(content) + self.isContentViewHidden = isContentViewHidden } } } diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift new file mode 100644 index 0000000000..27fc0c64e5 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift @@ -0,0 +1,74 @@ +// +// AddEditBookmarkDialogCoordinatorViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine + +final class AddEditBookmarkDialogCoordinatorViewModel: ObservableObject { + @ObservedObject var bookmarkModel: BookmarkViewModel + @ObservedObject var folderModel: AddFolderViewModel + @Published var viewState: ViewState + + private var cancellables: Set = [] + + init(bookmarkModel: BookmarkViewModel, folderModel: AddFolderViewModel) { + self.bookmarkModel = bookmarkModel + self.folderModel = folderModel + viewState = .bookmark + bind() + } + + func dismissAction() { + viewState = .bookmark + } + + func addFolderAction() { + folderModel.selectedFolder = bookmarkModel.selectedFolder + viewState = .folder + } + + private func bind() { + bookmarkModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.addFolderPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] bookmarkFolder in + self?.bookmarkModel.selectedFolder = bookmarkFolder + } + .store(in: &cancellables) + } +} + +extension AddEditBookmarkDialogCoordinatorViewModel { + enum ViewState { + case bookmark + case folder + } +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift new file mode 100644 index 0000000000..b511c17961 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift @@ -0,0 +1,182 @@ +// +// AddEditBookmarkDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@MainActor +protocol BookmarkDialogEditing: BookmarksDialogViewModel { + var bookmarkName: String { get set } + var bookmarkURLPath: String { get set } + var isBookmarkFavorite: Bool { get set } + + var isURLFieldHidden: Bool { get } +} + +@MainActor +final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { + + enum Mode { + case add(parentFolder: BookmarkFolder? = nil) + case edit(bookmark: Bookmark) + } + + @Published var bookmarkName: String + @Published var bookmarkURLPath: String + @Published var isBookmarkFavorite: Bool + + @Published private(set) var folders: [FolderViewModel] + @Published var selectedFolder: BookmarkFolder? + + private var folderCancellable: AnyCancellable? + + var title: String { + mode.title + } + + let isURLFieldHidden: Bool = false + + var cancelActionTitle: String { + mode.cancelActionTitle + } + + var defaultActionTitle: String { + mode.defaultActionTitle + } + + private var hasValidInput: Bool { + guard let url = bookmarkURLPath.url else { return false } + return !bookmarkName.trimmingWhitespace().isEmpty && url.isValid + } + + let isOtherActionDisabled: Bool = false + + var isDefaultActionDisabled: Bool { !hasValidInput } + + private let mode: Mode + private let bookmarkManager: BookmarkManager + + init(mode: Mode, bookmarkManager: LocalBookmarkManager = .shared) { + self.mode = mode + self.bookmarkManager = bookmarkManager + bookmarkName = mode.bookmark?.title ?? "" + bookmarkURLPath = mode.bookmark?.url ?? "" + isBookmarkFavorite = mode.bookmark?.isFavorite ?? false + folders = .init(bookmarkManager.list) + switch mode { + case let .add(parentFolder): + selectedFolder = parentFolder + case let .edit(bookmark): + selectedFolder = folders.first(where: { $0.id == bookmark.parentFolderUUID })?.entity + } + bind() + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + guard let url = bookmarkURLPath.url else { + assertionFailure("Invalid URL, default action button should be disabled.") + return + } + + let trimmedBookmarkName = bookmarkName.trimmingWhitespace() + + switch mode { + case .add: + addBookmark(withURL: url, name: trimmedBookmarkName, isFavorite: isBookmarkFavorite, to: selectedFolder) + case let .edit(bookmark): + updateBookmark(bookmark, url: url, name: trimmedBookmarkName, isFavorite: isBookmarkFavorite, location: selectedFolder) + } + dismiss() + } +} + +private extension AddEditBookmarkDialogViewModel { + + func bind() { + folderCancellable = bookmarkManager.listPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { bookmarkList in + self.folders = .init(bookmarkList) + }) + } + + func updateBookmark(_ bookmark: Bookmark, url: URL, name: String, isFavorite: Bool, location: BookmarkFolder?) { + var bookmark = bookmark + + // If URL changed update URL first as updating the Bookmark altogether will throw an error as the bookmark can't be fetched by URL. + if bookmark.url != url.absoluteString { + bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark + } + + if bookmark.title != name || bookmark.isFavorite != isBookmarkFavorite { + bookmark.title = name + bookmark.isFavorite = isBookmarkFavorite + bookmarkManager.update(bookmark: bookmark) + } + if bookmark.parentFolderUUID != selectedFolder?.id { + let parentFoler: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: parentFoler, completion: { _ in }) + } + } + + func addBookmark(withURL url: URL, name: String, isFavorite: Bool, to parent: BookmarkFolder?) { + bookmarkManager.makeBookmark(for: url, title: name, isFavorite: isFavorite, index: nil, parent: parent) + } +} + +private extension AddEditBookmarkDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addBookmark + case .edit: + return UserText.Bookmarks.Dialog.Title.editBookmark + } + } + + var cancelActionTitle: String { + switch self { + case .add, .edit: + return UserText.cancel + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addBookmark + case .edit: + return UserText.save + } + } + + var bookmark: Bookmark? { + switch self { + case .add: + return nil + case let .edit(bookmark): + return bookmark + } + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift index a1da3c5262..62c1e0356c 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift @@ -20,18 +20,13 @@ import Foundation import Combine @MainActor -protocol BookmarkFolderDialogViewModel: ObservableObject { - var title: String { get } - var folderName: String { get } - var folders: [FolderViewModel] { get } - var selectedFolder: BookmarkFolder? { get } - - func cancel() - func addOrSave() +protocol BookmarkFolderDialogEditing: BookmarksDialogViewModel { + var addFolderPublisher: AnyPublisher { get } + var folderName: String { get set } } @MainActor -final class AddEditBookmarkFolderDialogViewModel: ObservableObject { +final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { /// The type of operation to perform on a folder enum Mode { @@ -62,14 +57,19 @@ final class AddEditBookmarkFolderDialogViewModel: ObservableObject { mode.defaultActionTitle } - let isCancelActionDisabled = false + let isOtherActionDisabled = false - var isDefaultActionButtonDisabled: Bool { + var isDefaultActionDisabled: Bool { folderName.trimmingWhitespace().isEmpty } + var addFolderPublisher: AnyPublisher { + addFolderSubject.eraseToAnyPublisher() + } + private let mode: Mode private let bookmarkManager: BookmarkManager + private let addFolderSubject: PassthroughSubject = .init() init(mode: Mode, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { self.mode = mode @@ -124,7 +124,9 @@ private extension AddEditBookmarkFolderDialogViewModel { } func add(folderWithName name: String, to parent: BookmarkFolder?) { - bookmarkManager.makeFolder(for: name, parent: parent, completion: { _ in }) + bookmarkManager.makeFolder(for: name, parent: parent) { [weak self] bookmarkFolder in + self?.addFolderSubject.send(bookmarkFolder) + } } } diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift new file mode 100644 index 0000000000..08ef2cc47d --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift @@ -0,0 +1,35 @@ +// +// BookmarksDialogViewModel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@MainActor +protocol BookmarksDialogViewModel: ObservableObject { + var title: String { get } + + var folders: [FolderViewModel] { get } + var selectedFolder: BookmarkFolder? { get set } + + var cancelActionTitle: String { get } + var isOtherActionDisabled: Bool { get } + var defaultActionTitle: String { get } + var isDefaultActionDisabled: Bool { get } + + func cancel(dismiss: () -> Void) + func addOrSave(dismiss: () -> Void) +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift new file mode 100644 index 0000000000..53072513c4 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift @@ -0,0 +1,170 @@ +// +// AddEditBookmarkDialogCoordinatorViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkDialogCoordinatorViewModelTests: XCTestCase { + private var sut: AddEditBookmarkDialogCoordinatorViewModel! + private var bookmarkViewModelMock: AddEditBookmarkDialogViewModelMock! + private var bookmarkFolderViewModelMock: AddEditBookmarkFolderDialogViewModelMock! + private var cancellables: Set! + + override func setUpWithError() throws { + try super.setUpWithError() + + cancellables = [] + bookmarkViewModelMock = .init() + bookmarkFolderViewModelMock = .init() + sut = .init(bookmarkModel: bookmarkViewModelMock, folderModel: bookmarkFolderViewModelMock) + } + + override func tearDownWithError() throws { + cancellables = nil + bookmarkViewModelMock = nil + bookmarkFolderViewModelMock = nil + sut = nil + try super.tearDownWithError() + } + + func testShouldReturnViewStateBookmarkWhenInit() { + XCTAssertEqual(sut.viewState, .bookmark) + } + + func testShouldReturnViewStateBookmarkWhenDismissActionIsCalled() { + // GIVEN + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .folder) + + // WHEN + sut.dismissAction() + + // THEN + XCTAssertEqual(sut.viewState, .bookmark) + + } + + func testShouldSetSelectedFolderOnFolderViewModelAndReturnFolderViewStateWhenAddFolderActionIsCalled() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkViewModelMock.selectedFolder = folder + XCTAssertNil(bookmarkFolderViewModelMock.selectedFolder) + + // WHEN + sut.addFolderAction() + + // THEN + XCTAssertEqual(bookmarkFolderViewModelMock.selectedFolder, folder) + } + + func testShouldReceiveEventsWhenBookmarkModelChanges() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.bookmarkModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testShouldReceiveEventsWhenBookmarkFolderModelChanges() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.folderModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testShouldSetSelectedFolderOnBookmarkViewModelWhenAddFolderPublisherSendsEvent() { + // GIVEN + let expectation = self.expectation(description: #function) + bookmarkViewModelMock.selectedFolderExpectation = expectation + let folder = BookmarkFolder(id: "ABCDE", title: #function) + XCTAssertNil(bookmarkViewModelMock.selectedFolder) + + // WHEN + sut.folderModel.subject.send(folder) + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertEqual(bookmarkViewModelMock.selectedFolder, folder) + } + +} + +final class AddEditBookmarkDialogViewModelMock: BookmarkDialogEditing { + var bookmarkName: String = "" + var bookmarkURLPath: String = "" + var isBookmarkFavorite: Bool = false + var isURLFieldHidden: Bool = false + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { + didSet { + selectedFolderExpectation?.fulfill() + } + } + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + + var selectedFolderExpectation: XCTestExpectation? +} + +final class AddEditBookmarkFolderDialogViewModelMock: BookmarkFolderDialogEditing { + let subject = PassthroughSubject() + + var addFolderPublisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + var folderName: String = "" + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift new file mode 100644 index 0000000000..9268b39468 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -0,0 +1,591 @@ +// +// AddEditBookmarkDialogViewModelTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testReturnAddBookmarkTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.addBookmark) + } + + func testReturnEditBookmarkTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.editBookmark) + } + + func testReturnCancelActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnCancelActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnAddBookmarkActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addBookmark) + } + + func testReturnSaveActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.save) + } + + // MARK: State + + func testShouldSetBookmarkNameToEmptyWhenInitAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.bookmarkName + + // THEN + XCTAssertTrue(result.isEmpty) + } + + func testShouldSetBookmarkNameToValueWhenInitAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.bookmarkName + + // THEN + XCTAssertEqual(result, #function) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetSelectedFolderToNilWhenBookmarkParentFolderIsNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "2") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "2") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "1") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + // MARK: - Actions + + func testReturnIsCancelActionDisabledFalseWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsCancelActionDisabledFalseWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkNameIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkNameIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = "" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkNameIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkNameIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + // ------- + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarURLIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkURLIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkURLIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkURLIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testShouldCallDismissWhenCancelIsCalled() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldCallDismissWhenAddOrSaveIsCalled() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + var didCallDismiss = false + + // WHEN + sut.addOrSave { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldAskBookmarkStoreToSaveBookmarkWhenModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.bookmarkName = #function + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.title, #function) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) + } + + func testShouldAskBookmarkStoreToUpdateURLWhenURLIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: "DuckDuckGo", isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + sut.bookmarkURLPath = expectedBookmark.url + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateURLWhenURLIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenNameIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkName = expectedBookmark.title + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenNameIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkName = #function + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.isBookmarkFavorite = expectedBookmark.isFavorite + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.isBookmarkFavorite = false + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsDifferentFromOriginalFolderAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder") + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [folder, bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [bookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: folder.id)) + } + + func testShouldNotAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNotDifferentFromOriginalFolderAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "ABCDE") + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder", children: [bookmark]) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift index 135fc69cbf..a48f18d2ab 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift @@ -220,7 +220,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) // WHEN - let result = sut.isCancelActionDisabled + let result = sut.isOtherActionDisabled // THEN XCTAssertFalse(result) @@ -231,7 +231,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) // WHEN - let result = sut.isCancelActionDisabled + let result = sut.isOtherActionDisabled // THEN XCTAssertFalse(result) @@ -243,7 +243,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { sut.folderName = "" // WHEN - let result = sut.isDefaultActionButtonDisabled + let result = sut.isDefaultActionDisabled // THEN XCTAssertTrue(result) @@ -255,7 +255,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { sut.folderName = "" // WHEN - let result = sut.isDefaultActionButtonDisabled + let result = sut.isDefaultActionDisabled // THEN XCTAssertTrue(result) @@ -267,7 +267,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { sut.folderName = " Test " // WHEN - let result = sut.isDefaultActionButtonDisabled + let result = sut.isDefaultActionDisabled // THEN XCTAssertFalse(result) @@ -279,7 +279,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { sut.folderName = " Test " // WHEN - let result = sut.isDefaultActionButtonDisabled + let result = sut.isDefaultActionDisabled // THEN XCTAssertFalse(result) @@ -303,9 +303,10 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { // GIVEN let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) var didCallDismiss = false + sut.folderName = "DuckDuckGo" // WHEN - sut.cancel { + sut.addOrSave { didCallDismiss = true } @@ -385,7 +386,6 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { func testShouldAskBookmarkStoreToMoveFolderToRootFolderWhenSelectedFolderIsDifferentFromOriginalFolder() { // GIVEN - let location = BookmarkFolder(id: #file, title: #function) let folder = BookmarkFolder.mock let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) sut.selectedFolder = nil @@ -405,7 +405,6 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { func testShouldNotAskBookmarkStoreToMoveFolderWhenSelectedFolderIsNotDifferentFromOriginalFolder() { // GIVEN - let location = BookmarkFolder(id: #file, title: #function) let folder = BookmarkFolder.mock let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) sut.selectedFolder = nil From ff1cdfb4278bf94adee479780350c9a352ab6815 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 27 Feb 2024 10:54:11 +1100 Subject: [PATCH 05/19] Bookmarks shortcut panel (#2245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206670741563549/f **Description**: 1. “Three dots hover” menu button for Bookmarks and Folders in the bookmarks shortcut panel. 2. Add “Star" icon for favorite bookmarks. 3. Enhance Contextual Menu with "Edit…” for both folders and bookmarks. 4. Present Add/Edit Bookmarks/Folder dialogs. --- DuckDuckGo.xcodeproj/project.pbxproj | 8 ++ .../Model/BookmarkOutlineViewDataSource.swift | 12 +++ .../Bookmarks/Services/ContextualMenu.swift | 34 ++++++- .../Services/MenuItemSelectors.swift | 1 + .../View/BookmarkListViewController.swift | 62 ++++++++++--- ...okmarkManagementDetailViewController.swift | 14 ++- ...kmarkManagementSidebarViewController.swift | 4 + .../View/BookmarkOutlineCellView.swift | 91 +++++++++++++++++-- .../Bookmarks/View/BookmarkTableRowView.swift | 2 +- .../Dialog/BookmarksDialogViewFactory.swift | 75 +++++++++++++++ .../AddEditBookmarkDialogViewModel.swift | 43 ++++++--- .../Common/Extensions/NSMenuExtension.swift | 10 ++ .../BookmarkOutlineViewDataSourceTests.swift | 22 +++++ .../AddEditBookmarkDialogViewModelTests.swift | 49 +++++++++- 14 files changed, 377 insertions(+), 50 deletions(-) create mode 100644 DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9657c8b349..0910958a60 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2430,6 +2430,9 @@ 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F56CFB32B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; @@ -4042,6 +4045,7 @@ 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogView.swift; sourceTree = ""; }; 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; + 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; @@ -6731,6 +6735,7 @@ 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */, 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */, + 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */, ); path = Dialog; sourceTree = ""; @@ -10332,6 +10337,7 @@ 3706FC01293F65D500E42796 /* ChromiumBookmarksReader.swift in Sources */, 3706FC02293F65D500E42796 /* Downloads.xcdatamodeld in Sources */, B60C6F7829B0E286007BFAA8 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, + 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */, 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */, 4B9754EC2984300100D7B834 /* EmailManagerExtension.swift in Sources */, @@ -11307,6 +11313,7 @@ 4B957A542AC7AE700062CA31 /* VisitMenuItem.swift in Sources */, 4B957A552AC7AE700062CA31 /* EncryptionKeyStore.swift in Sources */, 4B957A562AC7AE700062CA31 /* TabExtensionsBuilder.swift in Sources */, + 9F56CFB32B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 1E2AE4C82ACB216B00684E0A /* HoverTrackingArea.swift in Sources */, 4B957A582AC7AE700062CA31 /* PasswordManagementIdentityItemView.swift in Sources */, 4B957A592AC7AE700062CA31 /* ProgressExtension.swift in Sources */, @@ -12121,6 +12128,7 @@ 4B6785472AA8DE68008A5004 /* NetworkProtectionFeatureDisabler.swift in Sources */, 4B0526642B1D55D80054955A /* VPNFeedbackCategory.swift in Sources */, 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */, + 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 4B723E0D26B0006100E14D75 /* SecureVaultLoginImporter.swift in Sources */, B645D8F629FA95440024461F /* WKProcessPoolExtension.swift in Sources */, 4B9292D32667123700AD2C21 /* AddBookmarkModalView.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index edfcfa6e2e..790597ae0a 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -34,6 +34,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS private let contentMode: ContentMode private let bookmarkManager: BookmarkManager + private let onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)? private let presentFaviconsFetcherOnboarding: (() -> Void)? private var favoritesPseudoFolder = PseudoFolder.favorites @@ -43,11 +44,13 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS contentMode: ContentMode, bookmarkManager: BookmarkManager, treeController: BookmarkTreeController, + onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)? = nil, presentFaviconsFetcherOnboarding: (() -> Void)? = nil ) { self.contentMode = contentMode self.bookmarkManager = bookmarkManager self.treeController = treeController + self.onMenuRequestedAction = onMenuRequestedAction self.presentFaviconsFetcherOnboarding = presentFaviconsFetcherOnboarding super.init() @@ -123,6 +126,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS } let cell = outlineView.makeView(withIdentifier: .init(BookmarkOutlineCellView.className()), owner: self) as? BookmarkOutlineCellView ?? BookmarkOutlineCellView(identifier: .init(BookmarkOutlineCellView.className())) + cell.delegate = self if let bookmark = node.representedObject as? Bookmark { cell.update(from: bookmark) @@ -329,3 +333,11 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS } } + +// MARK: - BookmarkOutlineCellViewDelegate + +extension BookmarkOutlineViewDataSource: BookmarkOutlineCellViewDelegate { + func outlineCellViewRequestedMenu(_ cell: BookmarkOutlineCellView) { + onMenuRequestedAction?(cell) + } +} diff --git a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift index 49f694be2b..36ee80bafe 100644 --- a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift @@ -22,6 +22,17 @@ struct ContextualMenu { // Not all contexts support an editing option for bookmarks. The option is displayed by default, but `includeBookmarkEditMenu` can disable it. static func menu(for objects: [Any]?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { + menu(for: objects, target: nil, includeBookmarkEditMenu: includeBookmarkEditMenu) + } + + /// Creates an instance of NSMenu for the specified Objects and target. + /// - Parameters: + /// - objects: The objects to create the menu for + /// - target: The target to associate to the `NSMenuItem` + /// - includeBookmarkEditMenu: True if menu should return edit bookmark option. False otherwise. Default is true + /// - Returns: An instance of NSMenu or nil if `objects` is not a `Bookmark` or a `Folder`. + static func menu(for objects: [Any]?, target: AnyObject?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { + guard let objects = objects, objects.count > 0 else { return menuForNoSelection() } @@ -33,13 +44,23 @@ struct ContextualMenu { let node = objects.first as? BookmarkNode let object = node?.representedObject ?? objects.first as? BaseBookmarkEntity + let menu: NSMenu? + if let bookmark = object as? Bookmark { - return menu(for: bookmark, includeBookmarkEditMenu: includeBookmarkEditMenu) + menu = self.menu(for: bookmark, includeBookmarkEditMenu: includeBookmarkEditMenu) } else if let folder = object as? BookmarkFolder { - return menu(for: folder) + // When the user edits a folder we need to show the parent in the folder picker. Folders directly child of PseudoFolder `Bookmarks` have nil parent because their parent is not an instance of `BookmarkFolder` + let parent = node?.parent?.representedObject as? BookmarkFolder + menu = self.menu(for: folder, parent: parent) } else { - return nil + menu = nil + } + + menu?.items.forEach { item in + item.target = target } + + return menu } // MARK: - Single Item Menu Creation @@ -75,10 +96,11 @@ struct ContextualMenu { return menu } - private static func menu(for folder: BookmarkFolder) -> NSMenu { + private static func menu(for folder: BookmarkFolder, parent: BookmarkFolder?) -> NSMenu { let menu = NSMenu(title: "") menu.addItem(renameFolderMenuItem(folder: folder)) + menu.addItem(editFolderMenuItem(folder: folder, parent: parent)) menu.addItem(deleteFolderMenuItem(folder: folder)) menu.addItem(NSMenuItem.separator()) @@ -97,6 +119,10 @@ struct ContextualMenu { return menuItem(UserText.renameFolder, #selector(FolderMenuItemSelectors.renameFolder(_:)), folder) } + static func editFolderMenuItem(folder: BookmarkFolder, parent: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.editBookmark, #selector(FolderMenuItemSelectors.editFolder(_:)), (folder, parent)) + } + static func deleteFolderMenuItem(folder: BookmarkFolder) -> NSMenuItem { return menuItem(UserText.deleteFolder, #selector(FolderMenuItemSelectors.deleteFolder(_:)), folder) } diff --git a/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift b/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift index 44114fc9d8..b6cf86e6b1 100644 --- a/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift +++ b/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift @@ -34,6 +34,7 @@ import AppKit func newFolder(_ sender: NSMenuItem) func renameFolder(_ sender: NSMenuItem) + func editFolder(_ sender: NSMenuItem) func deleteFolder(_ sender: NSMenuItem) func openInNewTabs(_ sender: NSMenuItem) diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 541ad955b6..b598421312 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -63,6 +63,9 @@ final class BookmarkListViewController: NSViewController { contentMode: .bookmarksAndFolders, bookmarkManager: bookmarkManager, treeController: treeController, + onMenuRequestedAction: { [weak self] cell in + self?.showContextMenu(for: cell) + }, presentFaviconsFetcherOnboarding: { [weak self] in guard let self, let window = self.view.window else { return @@ -334,18 +337,13 @@ final class BookmarkListViewController: NSViewController { } @objc func newBookmarkButtonClicked(_ sender: AnyObject) { - delegate?.popover(shouldPreventClosure: true) - AddBookmarkModalView(model: AddBookmarkModalViewModel(currentTabWebsite: currentTabWebsite) { [weak delegate] _ in - delegate?.popover(shouldPreventClosure: false) - }).show(in: parent?.view.window) + let view = BookmarksDialogViewFactory.makeAddBookmarkView(currentTab: currentTabWebsite) + showDialog(view: view) } @objc func newFolderButtonClicked(_ sender: AnyObject) { - delegate?.popover(shouldPreventClosure: true) - AddBookmarkFolderModalView() - .show(in: parent?.view.window) { [weak delegate] in - delegate?.popover(shouldPreventClosure: false) - } + let view = BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil) + showDialog(view: view) } @objc func openManagementInterface(_ sender: NSButton) { @@ -425,6 +423,30 @@ final class BookmarkListViewController: NSViewController { outlineView.selectRowIndexes(indexes, byExtendingSelection: false) } + private func showContextMenu(for cell: BookmarkOutlineCellView) { + let row = outlineView.row(for: cell) + guard + let item = outlineView.item(atRow: row), + let contextMenu = ContextualMenu.menu(for: [item]) + else { + return + } + + contextMenu.popUpAtMouseLocation(in: view) + } + +} + +private extension BookmarkListViewController { + + func showDialog(view: any ModalView) { + delegate?.popover(shouldPreventClosure: true) + + view.show(in: parent?.view.window) { [weak delegate] in + delegate?.popover(shouldPreventClosure: false) + } + } + } // MARK: - Menu Item Selectors @@ -439,11 +461,11 @@ extension BookmarkListViewController: NSMenuDelegate { } if outlineView.selectedRowIndexes.contains(row) { - return ContextualMenu.menu(for: outlineView.selectedItems, includeBookmarkEditMenu: false) + return ContextualMenu.menu(for: outlineView.selectedItems) } if let item = outlineView.item(atRow: row) { - return ContextualMenu.menu(for: [item], includeBookmarkEditMenu: false) + return ContextualMenu.menu(for: [item]) } else { return nil } @@ -498,7 +520,13 @@ extension BookmarkListViewController: BookmarkMenuItemSelectors { } func editBookmark(_ sender: NSMenuItem) { - // Unsupported in the list view for the initial release. + guard let bookmark = sender.representedObject as? Bookmark else { + assertionFailure("Failed to retrieve Bookmark from Edit Bookmark context menu item") + return + } + + let view = BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + showDialog(view: view) } func copyBookmark(_ sender: NSMenuItem) { @@ -549,6 +577,16 @@ extension BookmarkListViewController: FolderMenuItemSelectors { } } + func editFolder(_ sender: NSMenuItem) { + guard let (folder, parent) = sender.representedObject as? (BookmarkFolder, BookmarkFolder?) else { + assertionFailure("Failed to retrieve Bookmark from Edit Folder context menu item") + return + } + + let view = BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: parent) + showDialog(view: view) + } + func deleteFolder(_ sender: NSMenuItem) { guard let folder = sender.representedObject as? BookmarkFolder else { assertionFailure("Failed to retrieve Bookmark from Delete Folder context menu item") diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index a9b03ede97..8492b1e5e2 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -647,14 +647,8 @@ extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate return } - if let contextMenu = ContextualMenu.menu(for: [bookmark]), let cursorLocation = self.view.window?.mouseLocationOutsideOfEventStream { - let convertedLocation = self.view.convert(cursorLocation, from: nil) - contextMenu.items.forEach { item in - item.target = self - } - - contextMenu.popUp(positioning: nil, at: convertedLocation, in: self.view) - } + guard let contextMenu = ContextualMenu.menu(for: [bookmark], target: self) else { return } + contextMenu.popUpAtMouseLocation(in: view) } func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) { @@ -748,6 +742,10 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { .show(in: view.window) } + func editFolder(_ sender: NSMenuItem) { + // https://app.asana.com/0/0/1206531304671952/f + } + func deleteFolder(_ sender: NSMenuItem) { guard let folder = sender.representedObject as? BookmarkFolder else { assertionFailure("Failed to retrieve Bookmark from Delete Folder context menu item") diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index 6d0fdd6860..be6c57f57f 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -305,6 +305,10 @@ extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { .show(in: view.window) } + func editFolder(_ sender: NSMenuItem) { + // https://app.asana.com/0/0/1206531304671948/f + } + func deleteFolder(_ sender: NSMenuItem) { guard let folder = sender.representedObject as? BookmarkFolder else { assertionFailure("Failed to retrieve Folder from Delete Folder context menu item") diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift index 08e6c56953..ebea3ee4f6 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift @@ -19,11 +19,22 @@ import AppKit import Foundation +protocol BookmarkOutlineCellViewDelegate: AnyObject { + func outlineCellViewRequestedMenu(_ cell: BookmarkOutlineCellView) +} + final class BookmarkOutlineCellView: NSTableCellView { private lazy var faviconImageView = NSImageView() private lazy var titleLabel = NSTextField(string: "Bookmark/Folder") private lazy var countLabel = NSTextField(string: "42") + private lazy var menuButton = NSButton(title: "", image: .settings, target: self, action: #selector(cellMenuButtonClicked)) + private lazy var favoriteImageView = NSImageView() + private lazy var trackingArea: NSTrackingArea = { + NSTrackingArea(rect: .zero, options: [.inVisibleRect, .activeAlways, .mouseEnteredAndExited], owner: self, userInfo: nil) + }() + + weak var delegate: BookmarkOutlineCellViewDelegate? init(identifier: NSUserInterfaceItemIdentifier) { super.init(frame: .zero) @@ -34,10 +45,33 @@ final class BookmarkOutlineCellView: NSTableCellView { fatalError("\(type(of: self)): Bad initializer") } + override func updateTrackingAreas() { + super.updateTrackingAreas() + + guard !trackingAreas.contains(trackingArea) else { return } + addTrackingArea(trackingArea) + } + + override func mouseEntered(with event: NSEvent) { + countLabel.isHidden = true + favoriteImageView.isHidden = true + menuButton.isHidden = false + } + + override func mouseExited(with event: NSEvent) { + menuButton.isHidden = true + countLabel.isHidden = false + favoriteImageView.isHidden = false + } + + // MARK: - Private + private func setupUI() { addSubview(faviconImageView) addSubview(titleLabel) addSubview(countLabel) + addSubview(menuButton) + addSubview(favoriteImageView) faviconImageView.translatesAutoresizingMaskIntoConstraints = false faviconImageView.image = .bookmarkDefaultFavicon @@ -64,40 +98,74 @@ final class BookmarkOutlineCellView: NSTableCellView { countLabel.textColor = .blackWhite60 countLabel.lineBreakMode = .byClipping + menuButton.translatesAutoresizingMaskIntoConstraints = false + menuButton.contentTintColor = .buttonColor + menuButton.imagePosition = .imageTrailing + menuButton.isBordered = false + menuButton.isHidden = true + + favoriteImageView.translatesAutoresizingMaskIntoConstraints = false + favoriteImageView.imageScaling = .scaleProportionallyDown setupLayout() } private func setupLayout() { - faviconImageView.heightAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.widthAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5).isActive = true - faviconImageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + NSLayoutConstraint.activate([ + faviconImageView.heightAnchor.constraint(equalToConstant: 16), + faviconImageView.widthAnchor.constraint(equalToConstant: 16), + faviconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), + faviconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + + titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 10), + bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 6), + + countLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + countLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 5), + trailingAnchor.constraint(equalTo: countLabel.trailingAnchor), + + menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + menuButton.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 5), + menuButton.trailingAnchor.constraint(equalTo: trailingAnchor), + menuButton.topAnchor.constraint(equalTo: topAnchor), + menuButton.bottomAnchor.constraint(equalTo: bottomAnchor), + menuButton.widthAnchor.constraint(equalToConstant: 28), + + favoriteImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + favoriteImageView.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 5), + favoriteImageView.trailingAnchor.constraint(equalTo: menuButton.trailingAnchor), + favoriteImageView.heightAnchor.constraint(equalToConstant: 15), + favoriteImageView.widthAnchor.constraint(equalToConstant: 15), + ]) + faviconImageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .horizontal) faviconImageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .vertical) - titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 10).isActive = true - bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6).isActive = true - titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 6).isActive = true titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) titleLabel.setContentHuggingPriority(.init(rawValue: 200), for: .horizontal) - countLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - countLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 5).isActive = true - trailingAnchor.constraint(equalTo: countLabel.trailingAnchor).isActive = true countLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) countLabel.setContentHuggingPriority(.required, for: .horizontal) } + @objc private func cellMenuButtonClicked() { + delegate?.outlineCellViewRequestedMenu(self) + } + + // MARK: - Public + func update(from bookmark: Bookmark) { faviconImageView.image = bookmark.favicon(.small) ?? .bookmarkDefaultFavicon titleLabel.stringValue = bookmark.title countLabel.stringValue = "" + favoriteImageView.image = bookmark.isFavorite ? .favoriteFilledBorder : nil } func update(from folder: BookmarkFolder) { faviconImageView.image = .folder titleLabel.stringValue = folder.title + favoriteImageView.image = nil let totalChildBookmarks = folder.totalChildBookmarks if totalChildBookmarks > 0 { @@ -111,10 +179,13 @@ final class BookmarkOutlineCellView: NSTableCellView { faviconImageView.image = pseudoFolder.icon titleLabel.stringValue = pseudoFolder.name countLabel.stringValue = pseudoFolder.count > 0 ? String(pseudoFolder.count) : "" + favoriteImageView.image = nil } } +// MARK: - Preview + #if DEBUG @available(macOS 14.0, *) #Preview { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift index ebd3a9f318..079b44701d 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import Foundation +import AppKit final class BookmarkTableRowView: NSTableRowView { diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift new file mode 100644 index 0000000000..ac411dbfbf --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift @@ -0,0 +1,75 @@ +// +// BookmarksDialogViewFactory.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@MainActor +enum BookmarksDialogViewFactory { + + /// Creates an instance of AddEditBookmarkFolderDialogView for adding a Bookmark Folder. + /// - Parameters: + /// - parentFolder: An optional `BookmarkFolder`. When adding a folder to the root bookmark folder pass `nil`. For any other folder pass the `BookmarkFolder` the new folder should be within. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkFolderDialogView + static func makeAddBookmarkFolderView(parentFolder: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkFolderDialogView { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: parentFolder), bookmarkManager: bookmarkManager) + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + } + + /// Creates an instance of AddEditBookmarkFolderDialogView for editing a Bookmark Folder. + /// - Parameters: + /// - folder: The `BookmarkFolder` to edit. + /// - parentFolder: An optional `BookmarkFolder`. When editing a folder within the root bookmark folder pass `nil`. For any other folder pass the `BookmarkFolder` the folder belongs to. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkFolderDialogView + static func makeEditBookmarkFolderView(folder: BookmarkFolder, parentFolder: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkFolderDialogView { + let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: parentFolder), bookmarkManager: bookmarkManager) + return AddEditBookmarkFolderDialogView(viewModel: viewModel) + } + + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark. + /// - Parameters: + /// - currentTab: An optional `WebsiteInfo`. When adding a bookmark from the bookmark shortcut panel, if the `Tab` has loaded a web page pass the information via the `currentTab`. If the `Tab` has not loaded a tab pass `nil`. If adding a `Bookmark` from the `Manage Bookmark` settings page, pass `nil`. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView + static func makeAddBookmarkView(currentTab: WebsiteInfo?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: currentTab), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + + /// Creates an instance of AddEditBookmarkDialogView for editing a Bookmark. + /// - Parameters: + /// - bookmark: The `Bookmark` to edit. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView + static func makeEditBookmarkView(bookmark: Bookmark, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + +} + +private extension BookmarksDialogViewFactory { + + private static func makeAddEditBookmarkDialogView(viewModel: AddEditBookmarkDialogViewModel, bookmarkManager: BookmarkManager) -> AddEditBookmarkDialogView { + let addFolderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: viewModel, folderModel: addFolderViewModel) + return AddEditBookmarkDialogView(viewModel: viewModel) + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift index b511c17961..cced0cbd35 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift @@ -31,8 +31,14 @@ protocol BookmarkDialogEditing: BookmarksDialogViewModel { @MainActor final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { + /// The type of operation to perform on a bookmark. enum Mode { - case add(parentFolder: BookmarkFolder? = nil) + /// Add a new bookmark. Bookmarks can have a parent folder but not necessarily. + /// If the users add a bookmark to the root `Bookmarks` folder, then the parent folder is `nil`. + /// If the users add a bookmark to a different folder then the parent folder is not `nil`. + /// If the users add a bookmark from the bookmark shortcut and `Tab` has a page loaded, then the `tabWebsite` is not `nil`. + case add(tabWebsite: WebsiteInfo? = nil, parentFolder: BookmarkFolder? = nil) + /// Edit an existing bookmark. case edit(bookmark: Bookmark) } @@ -74,12 +80,12 @@ final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { init(mode: Mode, bookmarkManager: LocalBookmarkManager = .shared) { self.mode = mode self.bookmarkManager = bookmarkManager - bookmarkName = mode.bookmark?.title ?? "" - bookmarkURLPath = mode.bookmark?.url ?? "" - isBookmarkFavorite = mode.bookmark?.isFavorite ?? false + bookmarkName = mode.bookmarkName ?? "" + bookmarkURLPath = mode.bookmarkURLPath?.absoluteString ?? "" + isBookmarkFavorite = mode.bookmarkURLPath.flatMap(bookmarkManager.isUrlFavorited) ?? false folders = .init(bookmarkManager.list) switch mode { - case let .add(parentFolder): + case let .add(_, parentFolder): selectedFolder = parentFolder case let .edit(bookmark): selectedFolder = folders.first(where: { $0.id == bookmark.parentFolderUUID })?.entity @@ -126,12 +132,13 @@ private extension AddEditBookmarkDialogViewModel { if bookmark.url != url.absoluteString { bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark } - + // If the title or isFavorite changed, update the Bookmark if bookmark.title != name || bookmark.isFavorite != isBookmarkFavorite { bookmark.title = name bookmark.isFavorite = isBookmarkFavorite bookmarkManager.update(bookmark: bookmark) } + // If the bookmark changed parent location, move it. if bookmark.parentFolderUUID != selectedFolder?.id { let parentFoler: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: parentFoler, completion: { _ in }) @@ -139,7 +146,12 @@ private extension AddEditBookmarkDialogViewModel { } func addBookmark(withURL url: URL, name: String, isFavorite: Bool, to parent: BookmarkFolder?) { - bookmarkManager.makeBookmark(for: url, title: name, isFavorite: isFavorite, index: nil, parent: parent) + // If a bookmark already exist with the new URL, update it + if let existingBookmark = bookmarkManager.getBookmark(for: url) { + updateBookmark(existingBookmark, url: url, name: name, isFavorite: isFavorite, location: parent) + } else { + bookmarkManager.makeBookmark(for: url, title: name, isFavorite: isFavorite, index: nil, parent: parent) + } } } @@ -170,12 +182,21 @@ private extension AddEditBookmarkDialogViewModel.Mode { } } - var bookmark: Bookmark? { + var bookmarkName: String? { switch self { - case .add: - return nil + case let .add(tabInfo, _): + return tabInfo?.title + case let .edit(bookmark): + return bookmark.title + } + } + + var bookmarkURLPath: URL? { + switch self { + case let .add(tabInfo, _): + return tabInfo?.url case let .edit(bookmark): - return bookmark + return bookmark.urlObject } } diff --git a/DuckDuckGo/Common/Extensions/NSMenuExtension.swift b/DuckDuckGo/Common/Extensions/NSMenuExtension.swift index 69043e9a3b..6790d5c4f7 100644 --- a/DuckDuckGo/Common/Extensions/NSMenuExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSMenuExtension.swift @@ -58,4 +58,14 @@ extension NSMenu { insertItem(newItem, at: index) } + /// Pops up the menu at the current mouse location. + /// + /// - Parameter view: The view to display the menu item over. + /// - Attention: If the view is not currently installed in a window, this function does not show any pop up menu. + func popUpAtMouseLocation(in view: NSView) { + guard let cursorLocation = view.window?.mouseLocationOutsideOfEventStream else { return } + let convertedLocation = view.convert(cursorLocation, from: nil) + popUp(positioning: nil, at: convertedLocation, in: view) + } + } diff --git a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift index 8595ebdcc7..34172deede 100644 --- a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift @@ -159,6 +159,28 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { XCTAssertEqual(result, .none) } + func testWhenCellFiresDelegate_ThenOnMenuRequestedActionShouldFire() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + var didFireClosure = false + var capturedCell: BookmarkOutlineCellView? + let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: LocalBookmarkManager(), treeController: treeController) { cell in + didFireClosure = true + capturedCell = cell + } + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // WHEN + cell.delegate?.outlineCellViewRequestedMenu(cell) + + // THEN + XCTAssertTrue(didFireClosure) + XCTAssertEqual(cell, capturedCell) + } + // MARK: - Private private func createTreeController(with bookmarks: [BaseBookmarkEntity]) -> BookmarkTreeController { diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift index 9268b39468..3126fc360d 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -108,7 +108,7 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { // MARK: State - func testShouldSetBookmarkNameToEmptyWhenInitAndModeIsAdd() { + func testShouldSetBookmarkNameToEmptyWhenInitModeIsAddAndTabInfoIsNil() { // GIVEN let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) @@ -119,6 +119,20 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertTrue(result.isEmpty) } + func testShouldSetNameAndURLToValueWhenInitModeIsAddAndTabInfoIsNotNil() { + // GIVEN + let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + let url = sut.bookmarkURLPath + + // THEN + XCTAssertEqual(name, "Test") + XCTAssertEqual(url, URL.duckDuckGo.absoluteString) + } + func testShouldSetBookmarkNameToValueWhenInitAndModeIsEdit() { // GIVEN let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) @@ -296,8 +310,6 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertFalse(result) } - // ------- - func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarURLIsEmptyAndModeIsAdd() { // GIVEN let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) @@ -380,7 +392,36 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertTrue(didCallDismiss) } - func testShouldAskBookmarkStoreToSaveBookmarkWhenModeIsAdd() { + func testShouldAskBookmarkStoreToSaveBookmarkWhenModeIsAddAndURLIsNotAnExistingBookmark() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let existingBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + bookmarkStoreMock.bookmarks = [existingBookmark] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DDG" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [existingBookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: folder.id)) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.title, "DDG") + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.url, URL.duckDuckGo.absoluteString) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenModeIsAddAndURLIsAnExistingBookmark() { // GIVEN let folder = BookmarkFolder(id: #file, title: #function) let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) From 96d16f08a989f36acb8ddce65395b67713783a97 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 1 Mar 2024 16:09:00 +1100 Subject: [PATCH 06/19] Manage Bookmarks Changes (#2270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206687983236915/f **Description**: 1. Remove Favorites from Manage Bookmarks - Side view 2. Update Bookmarks root folder icon 3. Add “Star” icon to favorites. 4. Remove inline editing for bookmarks 5. Present Add/Edit bookmarks/folders dialog from CTA, Menu buttons and new “Edit…” contextual menu item --- .../AddBookmark.imageset/AddBookmark.svg | 8 + .../Images/AddBookmark.imageset/Contents.json | 2 +- .../icon-16-bookmark-add.pdf | Bin 3871 -> 0 bytes .../Images/AddFolder.imageset/AddFolder.svg | 8 + .../Images/AddFolder.imageset/Contents.json | 2 +- .../AddFolder.imageset/icon-16-folder-add.pdf | Bin 3057 -> 0 bytes .../Model/BookmarkOutlineViewDataSource.swift | 4 + .../Model/BookmarkSidebarTreeController.swift | 10 +- DuckDuckGo/Bookmarks/Model/PseudoFolder.swift | 2 +- .../Bookmarks/Services/ContextualMenu.swift | 38 ++- .../Services/LocalBookmarkStore.swift | 10 +- ...okmarkManagementDetailViewController.swift | 172 ++---------- ...kmarkManagementSidebarViewController.swift | 10 +- .../View/BookmarkOutlineCellView.swift | 6 +- .../View/BookmarkTableCellView.swift | 249 +++--------------- .../Bookmarks/View/BookmarkTableRowView.swift | 6 +- .../Dialog/BookmarksDialogViewFactory.swift | 12 +- .../AddEditBookmarkDialogViewModel.swift | 28 +- DuckDuckGo/Common/Localizables/UserText.swift | 4 +- DuckDuckGo/Localizable.xcstrings | 4 +- .../BookmarkOutlineViewDataSourceTests.swift | 30 +++ .../BookmarkSidebarTreeControllerTests.swift | 34 ++- .../AddEditBookmarkDialogViewModelTests.swift | 21 +- 23 files changed, 224 insertions(+), 436 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg delete mode 100644 DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg delete mode 100644 DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg new file mode 100644 index 0000000000..624b357638 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/AddBookmark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json index fd82163d37..b3519a0b69 100644 --- a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon-16-bookmark-add.pdf", + "filename" : "AddBookmark.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf b/DuckDuckGo/Assets.xcassets/Images/AddBookmark.imageset/icon-16-bookmark-add.pdf deleted file mode 100644 index 27e0c7b892a5446cbfb1350213f2e4b25779bb98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3871 zcmdT{TW{1x6n^)wm=`3d#O&NJ5{d)~L{*ER2@go94~t_$=w>(Fb*j)`-|vja$KGsO zklL48h>Sm<%Xcm_bN12OlUGl*j7_735}S`-8X=xN6N?uwHh1EKP*Rm=TIeuB)9P0! z;aQw@yUp#k?RLe)%e(&cykGX+@TSY3b_r8yuz7SO%D<|8e*3Zidee!;tIhVhVn285 z%l@-4;C5D_)9vQ=rr3H{q|5v8t*{uVoQuV~?Q*@lS#G;^`*rX8(NQ%u7rdXL=R(5=2*xX8BMk!z?hddy(Lob=AEHIya(h*WhJMAKn?2I>Az+Z}x7gk!O zvLRl;067KB1s#KeRLW5~k_sBh(LiEwCRrfx;J|?Nf-W&ybY!%~m3WNF8g!gg5v2%N zM5B^6BjT~JHo!Y4V2bWu}MpyG7&$d{8Tl%h|4z);&n znQC(|xM~vekkDu|<%AIFA*i?rVuF}T3)L!@r1s864bF^3B-K&FBuU{IBjDa!<)g?_ zc^7O1k`?kyxy;f;6Otz^p{FdQTu$0q7*J?`Yk;6_d`yJIsEjIl$vae!upAToWpsOC zpCno(qm5>WMX=~G@>iv%ZePtn-W5qw1EP%6hMLmId#rtE#kM}B+XTUlaOlg>5T6G_WCPFNf*Yv}`OXOZpdiw#hlRE!Pqfx+8ZV?~osLu1A;0vX(aBv4?> z@P9KI@%-@*6Oa2JPCUG~dGYOok42|x^17MU(+Zu=yNZ?ehMC$_Gi=hB&hiJ!PPwjS zw}z|k8KtV%A+gMz?{QO8d1zi7rp*UO-SgMW-EXr&m0tJe&}m%VUoB1rwOk!|_rD#y zEimamU)_9Oe%tg&_rL}Li(we%NdC^;II+XnxIe;Prs~L!Yir2RcTLpweqA87CmYKcXu3v8Zk{#=#Y92j$`R3JMbJ8YC diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg new file mode 100644 index 0000000000..b9b806e64d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/AddFolder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json index b75b5d095f..55db1eefb9 100644 --- a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon-16-folder-add.pdf", + "filename" : "AddFolder.svg", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf b/DuckDuckGo/Assets.xcassets/Images/AddFolder.imageset/icon-16-folder-add.pdf deleted file mode 100644 index ba53443ad58c36c58052d6ce25d057f1946ee063..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3057 zcmai0+in{-5Pj!Y@WntTAJp~6b|Fixq!ezyzGt}HS=tWT zg<;cYhlex6GqZDcb@AbiQAr3RwcP*qyO8qjTe-S?Jbk=AZBFC#pQ(R_jnpb@dgQxz z(~lT;H?&RIC;NXGcekfI=>V=_D2}JYxOot(i}A1fVO)QFFVC<0|AwRZQy5jbHBM=Z zH@xcKSzol3Ky1)C$l*b-*~`Uo*e$P(Q4X7om)#y?RZ2cZqBJSk;G}V?1g(vgRz++; zzzZ}8uc85*I&dq3ia9!yJ20vkoy&$G=S|L9CS_d634B)JP0CIVA}O0fDA;P1E-8lq zyAXZwUc%*KvNjG72ucAPLxUjZ<{+b6VSEkAgOO1aYD^T0R>|8Gv0|(!TO6GNN(O^M zI-HIYj2#$_38ijMF=uOKQ1H|ttFVf(BohEd@2$=gpEogt3^14CQXo6yoc2aWWvq7z zj(U|5h;x7(Uiw5^5r@yX8v>(tCX!vM>HvfjQbayFC?FePo3>7NNtNUad_oke=8CT2 zB{p5PF}Up=WqC0!EMFelKwoW|Dj&-bWaRU z`kg@5We-BYI#{n!s9x#pT|vf?lvwMeD+$KvYUb);a0?K5v98}uLW3Y{l^K(RFAy3! zhO^KfRA5W?7S~g2!ytuJ$kYrXL!id5GJA%uwRP(N!36VNR+j0t$buh^UsO2KndK9~s@VS-+$GXXQbG-qUhmeKR3 zc--tVMV-)(AP>eMo!JV-m{H*!(g+68VU2*9pzn!*&trtXL+vFmwhP7^o?w;V!NGP= zcXV@ykwG}>AQ|0=F%FC!>=2HJvCc1}e4z&=(VQmDH-5&nszzmTutS)?!&+9EiI3*< z7@>ID(Y)9$7%qOw^b@Q(*fLQY^kl^%;<_UKmQ}_$u;yT1!sqk4d+hw;>+3KsYMQ#* zNHx!2G-^%r8@?Hl$1?QiZmj!ux7(kN^3$&v^YE;G`TLJiuC6w>V*`F1Z|*nm_mA?^ zQn@U9!!lS_Gt+uJ?!O#{@hEAKTsOM&{q8jGa3W2IGkCQ*0F$9Xm=5O{2wG;)@9%Hm zs-y_kaz5vL{{?%^3;&NmIwpK0JEkej^UZ0q-QTvY6!h_HPCOpQ&jJ&In6yH_C8ie_ zB;fWKG&_1EX3O>@_(=~#_}IgXXOP8b`6-kz%07qiEt#RyVROG74}ymB?=FyzSI7Os s_*%ZYzj-|+WwqY#Pm%(z30_^_|1-k-I&^Oiry7pUE{L Void)? private let presentFaviconsFetcherOnboarding: (() -> Void)? @@ -44,12 +45,14 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS contentMode: ContentMode, bookmarkManager: BookmarkManager, treeController: BookmarkTreeController, + showMenuButtonOnHover: Bool = true, onMenuRequestedAction: ((BookmarkOutlineCellView) -> Void)? = nil, presentFaviconsFetcherOnboarding: (() -> Void)? = nil ) { self.contentMode = contentMode self.bookmarkManager = bookmarkManager self.treeController = treeController + self.showMenuButtonOnHover = showMenuButtonOnHover self.onMenuRequestedAction = onMenuRequestedAction self.presentFaviconsFetcherOnboarding = presentFaviconsFetcherOnboarding @@ -126,6 +129,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS } let cell = outlineView.makeView(withIdentifier: .init(BookmarkOutlineCellView.className()), owner: self) as? BookmarkOutlineCellView ?? BookmarkOutlineCellView(identifier: .init(BookmarkOutlineCellView.className())) + cell.shouldShowMenuButton = showMenuButtonOnHover cell.delegate = self if let bookmark = node.representedObject as? Bookmark { diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift b/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift index 06167f2356..b8549cad89 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkSidebarTreeController.swift @@ -33,19 +33,11 @@ final class BookmarkSidebarTreeController: BookmarkTreeControllerDataSource { // MARK: - Private private func childNodesForRootNode(_ node: BookmarkNode) -> [BookmarkNode] { - let favorites = PseudoFolder.favorites - let favoritesNode = BookmarkNode(representedObject: favorites, parent: node) - favoritesNode.canHaveChildNodes = false - - let blankSpacer = SpacerNode.blank - let spacerNode = BookmarkNode(representedObject: blankSpacer, parent: node) - spacerNode.canHaveChildNodes = false - let bookmarks = PseudoFolder.bookmarks let bookmarksNode = BookmarkNode(representedObject: bookmarks, parent: node) bookmarksNode.canHaveChildNodes = true - return [favoritesNode, spacerNode, bookmarksNode] + return [bookmarksNode] } private func childNodes(for parentNode: BookmarkNode) -> [BookmarkNode] { diff --git a/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift b/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift index c3aed14be7..85cef1888a 100644 --- a/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift +++ b/DuckDuckGo/Bookmarks/Model/PseudoFolder.swift @@ -22,7 +22,7 @@ import Foundation final class PseudoFolder: Equatable { static let favorites = PseudoFolder(id: UUID().uuidString, name: UserText.favorites, icon: .favoriteFilledBorder) - static let bookmarks = PseudoFolder(id: UUID().uuidString, name: UserText.bookmarks, icon: .folder) + static let bookmarks = PseudoFolder(id: UUID().uuidString, name: UserText.bookmarks, icon: .bookmarksFolder) let id: String let name: String diff --git a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift index 36ee80bafe..ff4426c209 100644 --- a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift @@ -27,9 +27,9 @@ struct ContextualMenu { /// Creates an instance of NSMenu for the specified Objects and target. /// - Parameters: - /// - objects: The objects to create the menu for + /// - objects: The objects to create the menu for. /// - target: The target to associate to the `NSMenuItem` - /// - includeBookmarkEditMenu: True if menu should return edit bookmark option. False otherwise. Default is true + /// - includeBookmarkEditMenu: True if menu should return edit bookmark option. False otherwise. Default is true. /// - Returns: An instance of NSMenu or nil if `objects` is not a `Bookmark` or a `Folder`. static func menu(for objects: [Any]?, target: AnyObject?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { @@ -42,24 +42,38 @@ struct ContextualMenu { } let node = objects.first as? BookmarkNode - let object = node?.representedObject ?? objects.first as? BaseBookmarkEntity + let object = node?.representedObject as? BaseBookmarkEntity ?? objects.first as? BaseBookmarkEntity + let parentFolder = node?.parent?.representedObject as? BookmarkFolder - let menu: NSMenu? + guard let object else { return nil } + + let menu = menu(for: object, parentFolder: parentFolder, includeBookmarkEditMenu: includeBookmarkEditMenu) + + menu?.items.forEach { item in + item.target = target + } - if let bookmark = object as? Bookmark { + return menu + } + + /// Creates an instance of NSMenu for the specified `BaseBookmarkEntity`and parent `BookmarkFolder`. + /// + /// - Parameters: + /// - entity: The bookmark entity to create the menu for. + /// - parentFolder: An optional `BookmarkFolder`. + /// - includeBookmarkEditMenu: True if menu should return edit bookmark option. False otherwise. Default is true. + /// - Returns: An instance of NSMenu or nil if `entity` is not a `Bookmark` or a `Folder`. + static func menu(for entity: BaseBookmarkEntity, parentFolder: BookmarkFolder?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { + let menu: NSMenu? + if let bookmark = entity as? Bookmark { menu = self.menu(for: bookmark, includeBookmarkEditMenu: includeBookmarkEditMenu) - } else if let folder = object as? BookmarkFolder { + } else if let folder = entity as? BookmarkFolder { // When the user edits a folder we need to show the parent in the folder picker. Folders directly child of PseudoFolder `Bookmarks` have nil parent because their parent is not an instance of `BookmarkFolder` - let parent = node?.parent?.representedObject as? BookmarkFolder - menu = self.menu(for: folder, parent: parent) + menu = self.menu(for: folder, parent: parentFolder) } else { menu = nil } - menu?.items.forEach { item in - item.target = target - } - return menu } diff --git a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift index 166eb51c02..e2be507a44 100644 --- a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift @@ -392,7 +392,10 @@ final class LocalBookmarkStore: BookmarkStore { func update(folder: BookmarkFolder) { do { - _ = try applyChangesAndSave(changes: { context in + _ = try applyChangesAndSave(changes: { [weak self] context in + guard let self = self else { + throw BookmarkStoreError.storeDeallocated + } try update(folder: folder, in: context) }) } catch { @@ -403,7 +406,10 @@ final class LocalBookmarkStore: BookmarkStore { func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) { do { - _ = try applyChangesAndSave(changes: { context in + _ = try applyChangesAndSave(changes: { [weak self] context in + guard let self = self else { + throw BookmarkStoreError.storeDeallocated + } let folderEntity = try update(folder: folder, in: context) try move(entities: [folderEntity], toIndex: nil, withinParentFolderType: parent, in: context) }) diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index 8492b1e5e2..a996cd2bf7 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -32,10 +32,6 @@ private struct EditedBookmarkMetadata { final class BookmarkManagementDetailViewController: NSViewController, NSMenuItemValidation { - fileprivate enum Constants { - static let animationSpeed: TimeInterval = 0.3 - } - private lazy var newBookmarkButton = MouseOverButton(title: " " + UserText.newBookmark, target: self, action: #selector(presentAddBookmarkModal)) private lazy var newFolderButton = MouseOverButton(title: " " + UserText.newFolder, target: self, action: #selector(presentAddFolderModal)) @@ -54,32 +50,10 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem private let bookmarkManager: BookmarkManager private var selectionState: BookmarkManagementSidebarViewController.SelectionState = .empty { didSet { - editingBookmarkIndex = nil reloadData() } } - private var isEditing: Bool { - return editingBookmarkIndex != nil - } - - private var editingBookmarkIndex: EditedBookmarkMetadata? { - didSet { - NSAnimationContext.runAnimationGroup { context in - context.allowsImplicitAnimation = true - context.duration = Constants.animationSpeed - - NSAppearance.withAppAppearance { - if editingBookmarkIndex != nil { - view.animator().layer?.backgroundColor = NSColor.backgroundSecondary.cgColor - } else { - view.animator().layer?.backgroundColor = NSColor.bookmarkPageBackground.cgColor - } - } - } - } - } - func update(selectionState: BookmarkManagementSidebarViewController.SelectionState) { self.selectionState = selectionState } @@ -195,7 +169,6 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem tableView.selectionHighlightStyle = .none tableView.allowsMultipleSelection = true tableView.usesAutomaticRowHeights = true - tableView.action = #selector(handleClick) tableView.doubleAction = #selector(handleDoubleClick) tableView.delegate = self tableView.dataSource = self @@ -264,15 +237,9 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem override func viewDidDisappear() { super.viewDidDisappear() - editingBookmarkIndex = nil reloadData() } - override func mouseUp(with event: NSEvent) { - // Clicking anywhere outside of the table view should end editing mode for a given cell. - updateEditingState(forRowAt: -1) - } - override func keyDown(with event: NSEvent) { if event.charactersIgnoringModifiers == String(UnicodeScalar(NSDeleteCharacter)!) { deleteSelectedItems() @@ -280,10 +247,6 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } fileprivate func reloadData() { - guard editingBookmarkIndex == nil else { - // If the table view is editing, the reload will be deferred until after the cell animation has completed. - return - } emptyState.isHidden = !(bookmarkManager.list?.topLevelEntities.isEmpty ?? true) let scrollPosition = tableView.visibleRect.origin @@ -306,7 +269,7 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem let index = sender.clickedRow - guard index != -1, editingBookmarkIndex?.index != index, let entity = fetchEntity(at: index) else { + guard index != -1, let entity = fetchEntity(at: index) else { return } @@ -324,21 +287,13 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } } - @objc func handleClick(_ sender: NSTableView) { - let index = sender.clickedRow - - if index != editingBookmarkIndex?.index { - endEditing() - } - } - @objc func presentAddBookmarkModal(_ sender: Any) { - AddBookmarkModalView(model: AddBookmarkModalViewModel(parent: selectionState.folder)) + BookmarksDialogViewFactory.makeAddBookmarkView(parent: selectionState.folder) .show(in: view.window) } @objc func presentAddFolderModal(_ sender: Any) { - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(parent: selectionState.folder)) + BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: selectionState.folder) .show(in: view.window) } @@ -354,53 +309,6 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem return true } - private func endEditing() { - if let editingIndex = editingBookmarkIndex?.index { - self.editingBookmarkIndex = nil - animateEditingState(forRowAt: editingIndex, editing: false) - } - } - - private func updateEditingState(forRowAt index: Int) { - guard index != -1 else { - endEditing() - return - } - - if editingBookmarkIndex?.index == nil || editingBookmarkIndex?.index != index { - endEditing() - } - - if let entity = fetchEntity(at: index) { - editingBookmarkIndex = EditedBookmarkMetadata(uuid: entity.id, index: index) - animateEditingState(forRowAt: index, editing: true) - } else { - assertionFailure("\(#file): Failed to find entity when updating editing state") - } - } - - private func animateEditingState(forRowAt index: Int, editing: Bool, completion: (() -> Void)? = nil) { - if let cell = tableView.view(atColumn: 0, row: index, makeIfNecessary: false) as? BookmarkTableCellView, - let row = tableView.rowView(atRow: index, makeIfNecessary: false) as? BookmarkTableRowView { - - tableView.beginUpdates() - NSAnimationContext.runAnimationGroup { context in - context.allowsImplicitAnimation = true - context.duration = Constants.animationSpeed - context.completionHandler = completion - - cell.editing = editing - row.editing = editing - - row.layoutSubtreeIfNeeded() - cell.layoutSubtreeIfNeeded() - tableView.noteHeightOfRows(withIndexesChanged: IndexSet(arrayLiteral: 0, index)) - } - - tableView.endUpdates() - } - } - private func totalRows() -> Int { switch selectionState { case .empty: @@ -444,12 +352,6 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi let rowView = BookmarkTableRowView() rowView.onSelectionChanged = onSelectionChanged - let entity = fetchEntity(at: row) - - if let uuid = editingBookmarkIndex?.uuid, uuid == entity?.id { - rowView.editing = true - } - return rowView } @@ -463,14 +365,12 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi if let bookmark = entity as? Bookmark { cell.update(from: bookmark) - cell.editing = bookmark.id == editingBookmarkIndex?.uuid if bookmark.favicon(.small) == nil { faviconsFetcherOnboarding?.presentOnboardingIfNeeded() } } else if let folder = entity as? BookmarkFolder { cell.update(from: folder) - cell.editing = folder.id == editingBookmarkIndex?.uuid } else { assertionFailure("Failed to cast bookmark") } @@ -573,6 +473,17 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } } + private func fetchEntityAndParent(at row: Int) -> (entity: BaseBookmarkEntity?, parentFolder: BookmarkFolder?) { + switch selectionState { + case .empty: + return (bookmarkManager.list?.topLevelEntities[safe: row], nil) + case .folder(let folder): + return (folder.children[safe: row], folder) + case .favorites: + return (bookmarkManager.list?.favoriteBookmarks[safe: row], nil) + } + } + private func index(for entity: Bookmark) -> Int? { switch selectionState { case .empty: @@ -638,8 +549,6 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate { func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) { - guard !isEditing else { return } - let row = tableView.row(for: cell) guard let bookmark = fetchEntity(at: row) as? Bookmark else { @@ -651,37 +560,6 @@ extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate contextMenu.popUpAtMouseLocation(in: view) } - func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) { - let row = tableView.row(for: cell) - - guard let bookmark = fetchEntity(at: row) as? Bookmark else { - assertionFailure("BookmarkManagementDetailViewController: Tried to favorite object which is not bookmark") - return - } - - bookmark.isFavorite.toggle() - bookmarkManager.update(bookmark: bookmark) - } - - func bookmarkTableCellView(_ cell: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) { - let row = tableView.row(for: cell) - defer { - endEditing() - } - guard var bookmark = fetchEntity(at: row) as? Bookmark, bookmark.id == uuid else { - return - } - - if let url = newUrl.url, url.absoluteString != bookmark.url { - bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark - } - let bookmarkTitle = (newTitle.isEmpty ? bookmark.title : newTitle).trimmingWhitespace() - if bookmark.title != bookmarkTitle { - bookmark.title = bookmarkTitle - bookmarkManager.update(bookmark: bookmark) - } - } - } // MARK: - NSMenuDelegate @@ -689,8 +567,6 @@ extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate extension BookmarkManagementDetailViewController: NSMenuDelegate { func contextualMenuForClickedRows() -> NSMenu? { - guard !isEditing else { return nil } - let row = tableView.clickedRow guard row != -1 else { @@ -701,8 +577,10 @@ extension BookmarkManagementDetailViewController: NSMenuDelegate { return ContextualMenu.menu(for: self.selectedItems()) } - if let item = fetchEntity(at: row) { - return ContextualMenu.menu(for: [item]) + let (item, parent) = fetchEntityAndParent(at: row) + + if let item { + return ContextualMenu.menu(for: item, parentFolder: parent) } else { return nil } @@ -743,7 +621,13 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { } func editFolder(_ sender: NSMenuItem) { - // https://app.asana.com/0/0/1206531304671952/f + guard let (folder, parent) = sender.representedObject as? (BookmarkFolder, BookmarkFolder?) else { + assertionFailure("Failed to cast menu represented object to BookmarkFolder") + return + } + + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: parent) + .show(in: view.window) } func deleteFolder(_ sender: NSMenuItem) { @@ -809,8 +693,10 @@ extension BookmarkManagementDetailViewController: BookmarkMenuItemSelectors { } func editBookmark(_ sender: NSMenuItem) { - guard let bookmark = sender.representedObject as? Bookmark, let bookmarkIndex = index(for: bookmark) else { return } - updateEditingState(forRowAt: bookmarkIndex) + guard let bookmark = sender.representedObject as? Bookmark else { return } + + BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + .show(in: view.window) } func copyBookmark(_ sender: NSMenuItem) { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index be6c57f57f..2c9b846841 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -51,7 +51,7 @@ final class BookmarkManagementSidebarViewController: NSViewController { private lazy var outlineView = BookmarksOutlineView(frame: scrollView.frame) private lazy var treeController = BookmarkTreeController(dataSource: treeControllerDataSource) - private lazy var dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) + private lazy var dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController, showMenuButtonOnHover: false) private var cancellables = Set() @@ -306,7 +306,13 @@ extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { } func editFolder(_ sender: NSMenuItem) { - // https://app.asana.com/0/0/1206531304671948/f + guard let (folder, parent) = sender.representedObject as? (BookmarkFolder, BookmarkFolder?) else { + assertionFailure("Failed to cast menu represented object to BookmarkFolder") + return + } + + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: parent) + .show(in: view.window) } func deleteFolder(_ sender: NSMenuItem) { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift index ebea3ee4f6..a50cc0ee06 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift @@ -34,6 +34,8 @@ final class BookmarkOutlineCellView: NSTableCellView { NSTrackingArea(rect: .zero, options: [.inVisibleRect, .activeAlways, .mouseEnteredAndExited], owner: self, userInfo: nil) }() + var shouldShowMenuButton = false + weak var delegate: BookmarkOutlineCellViewDelegate? init(identifier: NSUserInterfaceItemIdentifier) { @@ -48,17 +50,19 @@ final class BookmarkOutlineCellView: NSTableCellView { override func updateTrackingAreas() { super.updateTrackingAreas() - guard !trackingAreas.contains(trackingArea) else { return } + guard !trackingAreas.contains(trackingArea), shouldShowMenuButton else { return } addTrackingArea(trackingArea) } override func mouseEntered(with event: NSEvent) { + guard shouldShowMenuButton else { return } countLabel.isHidden = true favoriteImageView.isHidden = true menuButton.isHidden = false } override func mouseExited(with event: NSEvent) { + guard shouldShowMenuButton else { return } menuButton.isHidden = true countLabel.isHidden = false favoriteImageView.isHidden = false diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift index 195ad48845..cc7577766f 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift @@ -22,8 +22,6 @@ import Foundation @objc protocol BookmarkTableCellViewDelegate: AnyObject { func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) - func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) - func bookmarkTableCellView(_ cellView: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) } @@ -33,53 +31,18 @@ final class BookmarkTableCellView: NSTableCellView { private lazy var titleLabel = NSTextField(string: "Bookmark") private lazy var bookmarkURLLabel = NSTextField(string: "URL") - private lazy var favoriteButton = NSButton(title: "", image: .favoriteFilledBorder, target: self, action: #selector(favoriteButtonClicked)) private lazy var accessoryImageView = NSImageView(image: .forward) - private var favoriteButtonBottomConstraint: NSLayoutConstraint! - private var favoriteButtonTrailingConstraint: NSLayoutConstraint! - private lazy var containerView = NSView() - private lazy var shadowView = NSBox() private lazy var menuButton = NSButton(title: "", image: .settings, target: self, action: #selector(cellMenuButtonClicked)) - // Shadow view constraints: - - private var shadowViewTopConstraint: NSLayoutConstraint! - private var shadowViewBottomConstraint: NSLayoutConstraint! - - // Container view constraints: - - private var titleLabelTopConstraint: NSLayoutConstraint! - private var titleLabelBottomConstraint: NSLayoutConstraint! - @objc func cellMenuButtonClicked(_ sender: NSButton) { delegate?.bookmarkTableCellViewRequestedMenu(sender, cell: self) } - @objc func favoriteButtonClicked(_ sender: NSButton) { - guard entity is Bookmark else { - assertionFailure("\(#file): Tried to favorite non-Bookmark object") - return - } - - delegate?.bookmarkTableCellViewToggledFavorite(cell: self) - } - weak var delegate: BookmarkTableCellViewDelegate? - var editing: Bool = false { - didSet { - if editing { - enterEditingMode() - } else { - exitEditingMode() - } - updateColors() - } - } - var isSelected = false { didSet { updateColors() @@ -96,16 +59,14 @@ final class BookmarkTableCellView: NSTableCellView { return } - accessoryImageView.isHidden = mouseInside || editing - menuButton.isHidden = !mouseInside || editing + accessoryImageView.isHidden = mouseInside + menuButton.isHidden = !mouseInside - if !mouseInside && !editing { + if !mouseInside { resetAppearanceFromBookmark() } - if !editing { - updateTitleLabelValue() - } + updateTitleLabelValue() } } @@ -130,36 +91,17 @@ final class BookmarkTableCellView: NSTableCellView { fatalError("\(type(of: self)): Bad initializer") } - // swiftlint:disable:next function_body_length private func setupUI() { autoresizingMask = [.width, .height] - addSubview(shadowView) addSubview(containerView) - shadowView.boxType = .custom - shadowView.borderColor = .clear - shadowView.borderWidth = 1 - shadowView.cornerRadius = 4 - shadowView.fillColor = .tableCellEditing - shadowView.translatesAutoresizingMaskIntoConstraints = false - shadowView.wantsLayer = true - shadowView.layer?.backgroundColor = NSColor.tableCellEditing.cgColor - shadowView.layer?.cornerRadius = 6 - - let shadow = NSShadow() - shadow.shadowOffset = NSSize(width: 0, height: -1) - shadow.shadowColor = NSColor.black.withAlphaComponent(0.2) - shadow.shadowBlurRadius = 2.0 - shadowView.shadow = shadow containerView.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(faviconImageView) containerView.addSubview(titleLabel) containerView.addSubview(menuButton) containerView.addSubview(accessoryImageView) - containerView.addSubview(bookmarkURLLabel) - containerView.addSubview(favoriteButton) faviconImageView.contentTintColor = .suggestionIcon faviconImageView.wantsLayer = true @@ -176,92 +118,50 @@ final class BookmarkTableCellView: NSTableCellView { titleLabel.font = .systemFont(ofSize: 13) titleLabel.textColor = .labelColor titleLabel.lineBreakMode = .byTruncatingTail - titleLabel.cell?.sendsActionOnEndEditing = true titleLabel.cell?.usesSingleLineMode = true titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) titleLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - titleLabel.delegate = self - - bookmarkURLLabel.focusRingType = .none - bookmarkURLLabel.isEditable = false - bookmarkURLLabel.isSelectable = false - bookmarkURLLabel.isBordered = false - bookmarkURLLabel.drawsBackground = false - bookmarkURLLabel.font = .systemFont(ofSize: 13) - bookmarkURLLabel.textColor = .secondaryLabelColor - bookmarkURLLabel.lineBreakMode = .byClipping - bookmarkURLLabel.translatesAutoresizingMaskIntoConstraints = false - bookmarkURLLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - bookmarkURLLabel.setContentHuggingPriority(.required, for: .vertical) - bookmarkURLLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - bookmarkURLLabel.delegate = self accessoryImageView.translatesAutoresizingMaskIntoConstraints = false - accessoryImageView.widthAnchor.constraint(equalToConstant: 22).isActive = true - accessoryImageView.heightAnchor.constraint(equalToConstant: 32).isActive = true menuButton.contentTintColor = .button menuButton.translatesAutoresizingMaskIntoConstraints = false menuButton.isBordered = false menuButton.isHidden = true - - favoriteButton.translatesAutoresizingMaskIntoConstraints = false - favoriteButton.isBordered = false } private func setupLayout() { + NSLayoutConstraint.activate([ + trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 3), + containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3), + bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 3), + containerView.topAnchor.constraint(equalTo: topAnchor, constant: 3), - trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: 3).isActive = true - shadowView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3).isActive = true - containerView.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor).isActive = true - containerView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor).isActive = true - containerView.topAnchor.constraint(equalTo: shadowView.topAnchor).isActive = true - containerView.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor).isActive = true - - bookmarkURLLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10).isActive = true - favoriteButtonTrailingConstraint = trailingAnchor.constraint(equalTo: favoriteButton.trailingAnchor, constant: 3) - favoriteButtonTrailingConstraint.isActive = true + menuButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8), + faviconImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 6), - menuButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8).isActive = true - faviconImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 6).isActive = true - favoriteButton.topAnchor.constraint(equalTo: bookmarkURLLabel.bottomAnchor).isActive = true + accessoryImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 8), + trailingAnchor.constraint(equalTo: accessoryImageView.trailingAnchor, constant: 3), + faviconImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + trailingAnchor.constraint(equalTo: menuButton.trailingAnchor, constant: 2), - accessoryImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - titleLabel.leadingAnchor.constraint(equalTo: faviconImageView.trailingAnchor, constant: 8).isActive = true - trailingAnchor.constraint(equalTo: accessoryImageView.trailingAnchor, constant: 3).isActive = true - faviconImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - trailingAnchor.constraint(equalTo: menuButton.trailingAnchor, constant: 2).isActive = true - bookmarkURLLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor).isActive = true - trailingAnchor.constraint(equalTo: bookmarkURLLabel.trailingAnchor, constant: 16).isActive = true - menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true + menuButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), - favoriteButton.widthAnchor.constraint(equalToConstant: 24).isActive = true - favoriteButton.heightAnchor.constraint(equalToConstant: 24).isActive = true + menuButton.heightAnchor.constraint(equalToConstant: 32), + menuButton.widthAnchor.constraint(equalToConstant: 28), - menuButton.heightAnchor.constraint(equalToConstant: 32).isActive = true - menuButton.widthAnchor.constraint(equalToConstant: 28).isActive = true + faviconImageView.heightAnchor.constraint(equalToConstant: 16), + faviconImageView.widthAnchor.constraint(equalToConstant: 16), - faviconImageView.heightAnchor.constraint(equalToConstant: 16).isActive = true - faviconImageView.widthAnchor.constraint(equalToConstant: 16).isActive = true + bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5), - shadowViewTopConstraint = shadowView.topAnchor.constraint(equalTo: topAnchor, constant: 3) - shadowViewTopConstraint.isActive = true - - shadowViewBottomConstraint = bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: 3) - shadowViewBottomConstraint.isActive = true - - titleLabelBottomConstraint = bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8) - titleLabelBottomConstraint.priority = .init(rawValue: 250) - titleLabelBottomConstraint.isActive = true - - favoriteButtonBottomConstraint = bottomAnchor.constraint(equalTo: favoriteButton.bottomAnchor, constant: 8) - favoriteButtonBottomConstraint.priority = .init(rawValue: 750) - favoriteButtonBottomConstraint.isActive = true - - titleLabelTopConstraint = titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5) - titleLabelTopConstraint.isActive = true + accessoryImageView.widthAnchor.constraint(equalToConstant: 22), + accessoryImageView.heightAnchor.constraint(equalToConstant: 32), + ]) } override var backgroundStyle: NSView.BackgroundStyle { @@ -314,12 +214,10 @@ final class BookmarkTableCellView: NSTableCellView { accessoryImageView.isHidden = false } - accessoryImageView.image = bookmark.isFavorite ? .favorite : nil - favoriteButton.image = bookmark.isFavorite ? .favoriteFilledBorder : .favorite + accessoryImageView.image = bookmark.isFavorite ? .favoriteFilledBorder : nil titleLabel.stringValue = bookmark.title primaryTitleLabelValue = bookmark.title tertiaryTitleLabelValue = bookmark.url - bookmarkURLLabel.stringValue = bookmark.url } func update(from folder: BookmarkFolder) { @@ -333,65 +231,11 @@ final class BookmarkTableCellView: NSTableCellView { private func resetCellState() { self.entity = nil - editing = false mouseInside = false - bookmarkURLLabel.isHidden = true - favoriteButton.isHidden = true - titleLabelBottomConstraint.priority = .required - } - - private func enterEditingMode() { - titleLabel.isEditable = true - bookmarkURLLabel.isEditable = true - - shadowViewTopConstraint.constant = 10 - shadowViewBottomConstraint.constant = 10 - titleLabelTopConstraint.constant = 12 - favoriteButtonTrailingConstraint.constant = 11 - favoriteButtonBottomConstraint.constant = 18 - shadowView.isHidden = false - faviconImageView.isHidden = true - - bookmarkURLLabel.isHidden = false - favoriteButton.isHidden = false - titleLabelBottomConstraint.priority = .defaultLow - - hideTertiaryValueInTitleLabel() - - // Reluctantly use GCD as a workaround for a rare label layout issue, in which the text field shows no text upon becoming first responder. - DispatchQueue.main.async { - self.titleLabel.becomeFirstResponder() - } - } - - private func exitEditingMode() { - window?.makeFirstResponder(nil) - - titleLabel.isEditable = false - bookmarkURLLabel.isEditable = false - - titleLabelTopConstraint.constant = 5 - shadowViewTopConstraint.constant = 3 - shadowViewBottomConstraint.constant = 3 - favoriteButtonTrailingConstraint.constant = 3 - favoriteButtonBottomConstraint.constant = 8 - shadowView.isHidden = true - faviconImageView.isHidden = false - - bookmarkURLLabel.isHidden = true - favoriteButton.isHidden = true - titleLabelBottomConstraint.priority = .required - - if let editedBookmark = self.entity as? Bookmark { - delegate?.bookmarkTableCellView(self, - updatedBookmarkWithUUID: editedBookmark.id, - newTitle: titleLabel.stringValue, - newUrl: bookmarkURLLabel.stringValue) - } } private func updateColors() { - titleLabel.textColor = isSelected && !editing ? .white : .controlTextColor + titleLabel.textColor = isSelected ? .white : .controlTextColor menuButton.contentTintColor = isSelected ? .white : .button faviconImageView.contentTintColor = isSelected ? .white : .suggestionIcon accessoryImageView.contentTintColor = isSelected ? .white : .suggestionIcon @@ -428,11 +272,7 @@ final class BookmarkTableCellView: NSTableCellView { } private func updateTitleLabelValue() { - guard !editing else { - return - } - - if let tertiaryValue = tertiaryTitleLabelValue, mouseInside, !editing { + if let tertiaryValue = tertiaryTitleLabelValue, mouseInside { showTertiaryValueInTitleLabel(tertiaryValue) } else { hideTertiaryValueInTitleLabel() @@ -467,26 +307,6 @@ final class BookmarkTableCellView: NSTableCellView { } -extension BookmarkTableCellView: NSTextFieldDelegate { - - func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - switch commandSelector { - case #selector(cancelOperation) where self.editing: - self.resetAppearanceFromBookmark() - self.editing = false - return true - - case #selector(insertNewline) where self.editing: - self.editing = false - return true - - default: break - } - return false - } - -} - #if DEBUG @available(macOS 14.0, *) #Preview { @@ -517,19 +337,10 @@ extension BookmarkTableCellView { fatalError("init(coder:) has not been implemented") } - func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) { - cell.editing.toggle() - } + func bookmarkTableCellViewRequestedMenu(_ sender: NSButton, cell: BookmarkTableCellView) {} func bookmarkTableCellViewToggledFavorite(cell: BookmarkTableCellView) { (cell.entity as? Bookmark)?.isFavorite.toggle() - cell.editing = false - } - - func bookmarkTableCellView(_ cellView: BookmarkTableCellView, updatedBookmarkWithUUID uuid: String, newTitle: String, newUrl: String) { - if cell.editing { - cell.editing = false - } } } } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift index 079b44701d..0f06f84788 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableRowView.swift @@ -22,8 +22,6 @@ final class BookmarkTableRowView: NSTableRowView { var onSelectionChanged: (() -> Void)? - var editing = false - var hasPrevious = false { didSet { needsDisplay = true @@ -56,7 +54,7 @@ final class BookmarkTableRowView: NSTableRowView { backgroundColor.setFill() bounds.fill() - if mouseInside && !editing { + if mouseInside { let path = NSBezierPath(roundedRect: bounds, xRadius: 6, yRadius: 6) NSColor.rowHover.setFill() path.fill() @@ -68,8 +66,6 @@ final class BookmarkTableRowView: NSTableRowView { } override func drawSelection(in dirtyRect: NSRect) { - guard !editing else { return } - var roundedCorners = [NSBezierPath.Corners]() if !hasPrevious { diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift index ac411dbfbf..cffb72774e 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift @@ -42,7 +42,7 @@ enum BookmarksDialogViewFactory { return AddEditBookmarkFolderDialogView(viewModel: viewModel) } - /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark. + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark with the specified web page. /// - Parameters: /// - currentTab: An optional `WebsiteInfo`. When adding a bookmark from the bookmark shortcut panel, if the `Tab` has loaded a web page pass the information via the `currentTab`. If the `Tab` has not loaded a tab pass `nil`. If adding a `Bookmark` from the `Manage Bookmark` settings page, pass `nil`. /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. @@ -52,6 +52,16 @@ enum BookmarksDialogViewFactory { return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) } + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark with the specified parent folder. + /// - Parameters: + /// - parentFolder: An optional `BookmarkFolder`. When adding a bookmark from the bookmark management view, if the user select a parent folder pass this value won't be `nil`. Otherwise, if no folder is selected this value will be `nil`. + /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView + static func makeAddBookmarkView(parent: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: parent), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + /// Creates an instance of AddEditBookmarkDialogView for editing a Bookmark. /// - Parameters: /// - bookmark: The `Bookmark` to edit. diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift index cced0cbd35..e435aa9f46 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift @@ -80,16 +80,25 @@ final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { init(mode: Mode, bookmarkManager: LocalBookmarkManager = .shared) { self.mode = mode self.bookmarkManager = bookmarkManager - bookmarkName = mode.bookmarkName ?? "" - bookmarkURLPath = mode.bookmarkURLPath?.absoluteString ?? "" - isBookmarkFavorite = mode.bookmarkURLPath.flatMap(bookmarkManager.isUrlFavorited) ?? false + self.isBookmarkFavorite = mode.bookmarkURL.flatMap(bookmarkManager.isUrlFavorited) ?? false folders = .init(bookmarkManager.list) switch mode { - case let .add(_, parentFolder): + case let .add(websiteInfo, parentFolder): + // When adding a new bookmark with website info we need to show the bookmark name and URL only if the bookmark is not bookmarked already. + // Scenario we click on the "Add Bookmark" button from Bookmarks shortcut Panel. If Tab has a Bookmark loaded we present the dialog with prepopulated name and URL from the tab. + // If we save and click again on the "Add Bookmark" button we don't want to try re-add the same bookmark. Hence we present a dialog that is not pre-populated. + let isAlreadyBookmarked = websiteInfo.flatMap { bookmarkManager.isUrlBookmarked(url: $0.url) } ?? false + let websiteName = isAlreadyBookmarked ? "" : websiteInfo?.title ?? "" + let websiteURLPath = isAlreadyBookmarked ? "" : websiteInfo?.url.absoluteString ?? "" + bookmarkName = websiteName + bookmarkURLPath = websiteURLPath selectedFolder = parentFolder case let .edit(bookmark): + bookmarkName = bookmark.title + bookmarkURLPath = bookmark.urlObject?.absoluteString ?? "" selectedFolder = folders.first(where: { $0.id == bookmark.parentFolderUUID })?.entity } + bind() } @@ -182,16 +191,7 @@ private extension AddEditBookmarkDialogViewModel.Mode { } } - var bookmarkName: String? { - switch self { - case let .add(tabInfo, _): - return tabInfo?.title - case let .edit(bookmark): - return bookmark.title - } - } - - var bookmarkURLPath: URL? { + var bookmarkURL: URL? { switch self { case let .add(tabInfo, _): return tabInfo?.url diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 7187ef52d3..963a96a018 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1047,9 +1047,9 @@ struct UserText { enum Bookmarks { enum Dialog { enum Title { - static let addBookmark = NSLocalizedString("bookmarks.dialog.title.add", value: "Add bookmark", comment: "Bookmark creation dialog title") + static let addBookmark = NSLocalizedString("bookmarks.dialog.title.add", value: "Add Bookmark", comment: "Bookmark creation dialog title") static let addedBookmark = NSLocalizedString("bookmarks.dialog.title.added", value: "Bookmark Added", comment: "Bookmark added popover title") - static let editBookmark = NSLocalizedString("bookmarks.dialog.title.edit", value: "Edit bookmark", comment: "Bookmark edit dialog title") + static let editBookmark = NSLocalizedString("bookmarks.dialog.title.edit", value: "Edit Bookmark", comment: "Bookmark edit dialog title") static let addFolder = NSLocalizedString("bookmarks.dialog.folder.title.add", value: "Add Folder", comment: "Bookmark folder creation dialog title") static let editFolder = NSLocalizedString("bookmarks.dialog.folder.title.edit", value: "Edit Folder", comment: "Bookmark folder edit dialog title") } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 9347c6977d..47bd4f9d1c 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -8959,7 +8959,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Add bookmark" + "value" : "Add Bookmark" } }, "es" : { @@ -9079,7 +9079,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Edit bookmark" + "value" : "Edit Bookmark" } }, "es" : { diff --git a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift index 34172deede..2ec8d590de 100644 --- a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift @@ -181,6 +181,36 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { XCTAssertEqual(cell, capturedCell) } + func testWhenShowMenuButtonOnHoverIsTrue_ThenCellShouldHaveShouldMenuButtonFlagTrue() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + let dataSource = BookmarkOutlineViewDataSource(contentMode: .bookmarksAndFolders, bookmarkManager: LocalBookmarkManager(), treeController: treeController, showMenuButtonOnHover: true) + + // WHEN + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // THEN + XCTAssertTrue(cell.shouldShowMenuButton) + } + + func testWhenShowMenuButtonOnHoverIsFalse_ThenCellShouldHaveShouldMenuButtonFlagFalse() throws { + // GIVEN + let mockFolder = BookmarkFolder.mock + let mockOutlineView = NSOutlineView(frame: .zero) + let treeController = createTreeController(with: [mockFolder]) + let mockFolderNode = treeController.node(representing: mockFolder)! + let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: LocalBookmarkManager(), treeController: treeController, showMenuButtonOnHover: false) + + // WHEN + let cell = try XCTUnwrap(dataSource.outlineView(mockOutlineView, viewFor: nil, item: mockFolderNode) as? BookmarkOutlineCellView) + + // THEN + XCTAssertFalse(cell.shouldShowMenuButton) + } + // MARK: - Private private func createTreeController(with bookmarks: [BaseBookmarkEntity]) -> BookmarkTreeController { diff --git a/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift b/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift index 6c9e8c59da..4b95d7ca90 100644 --- a/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift @@ -28,24 +28,18 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let defaultNodes = treeController.rootNode.childNodes let representedObjects = defaultNodes.representedObjects() - // The sidebar defines three hardcoded nodes: + // The sidebar defines one hardcoded nodes: // - // 1. Favorites node - // 2. Spacer node - // 3. Bookmarks node + // 1. Bookmarks node - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - XCTAssertFalse(defaultNodes[0].canHaveChildNodes) - XCTAssertFalse(defaultNodes[1].canHaveChildNodes) - XCTAssertTrue(defaultNodes[2].canHaveChildNodes) + XCTAssertTrue(defaultNodes[0].canHaveChildNodes) - XCTAssert(representedObjects[0] === PseudoFolder.favorites) - XCTAssert(representedObjects[1] === SpacerNode.blank) - XCTAssert(representedObjects[2] === PseudoFolder.bookmarks) + XCTAssert(representedObjects.first === PseudoFolder.bookmarks) } - func testWhenBookmarkStoreHasNoTopLevelFolders_ThenTheDefaultBookmarksNodeHasNoChildren() { + func testWhenBookmarkStoreHasNoTopLevelFolders_ThenTheDefaultBookmarksNodeHasNoChildren() throws { let bookmarkStoreMock = BookmarkStoreMock() let faviconManagerMock = FaviconManagerMock() let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) @@ -56,11 +50,13 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) // The sidebar tree controller only shows folders, so if there are only bookmarks then the bookmarks default folder will be empty. - let bookmarksNode = defaultNodes[2] - XCTAssert(bookmarksNode.childNodes.isEmpty) + let bookmarksNode = defaultNodes[0] + let pseudoFolder = try XCTUnwrap(bookmarksNode.representedObject as? PseudoFolder) + XCTAssertTrue(bookmarksNode.childNodes.isEmpty) + XCTAssertEqual(pseudoFolder.name, "Bookmarks") } func testWhenBookmarkStoreHasTopLevelFolders_ThenTheDefaultBookmarksNodeHasThemAsChildren() { @@ -75,9 +71,9 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - let bookmarksNode = defaultNodes[2] + let bookmarksNode = defaultNodes[0] XCTAssertEqual(bookmarksNode.childNodes.count, 1) let childNode = bookmarksNode.childNodes[0] @@ -98,9 +94,9 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { let dataSource = BookmarkSidebarTreeController(bookmarkManager: bookmarkManager) let treeController = BookmarkTreeController(dataSource: dataSource) let defaultNodes = treeController.rootNode.childNodes - XCTAssertEqual(defaultNodes.count, 3) + XCTAssertEqual(defaultNodes.count, 1) - let bookmarksNode = defaultNodes[2] + let bookmarksNode = defaultNodes[0] XCTAssertTrue(bookmarksNode.canHaveChildNodes) XCTAssertEqual(bookmarksNode.childNodes.count, 1) diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift index 3126fc360d..18ad304a8c 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -119,7 +119,7 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertTrue(result.isEmpty) } - func testShouldSetNameAndURLToValueWhenInitModeIsAddAndTabInfoIsNotNil() { + func testShouldSetNameAndURLToValueWhenInitModeIsAddTabInfoIsNotNilAndURLIsNotAlreadyBookmarked() { // GIVEN let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) @@ -133,6 +133,24 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertEqual(url, URL.duckDuckGo.absoluteString) } + func testShouldSetNameAndURLToEmptyWhenInitModeIsAddTabInfoIsNotNilAndURLIsAlreadyBookmarked() throws { + // GIVEN + let tab = Tab(content: .url(URL.duckDuckGo, source: .link), title: "Test") + let websiteInfo = try XCTUnwrap(WebsiteInfo(tab)) + let bookmark = Bookmark(id: "1", url: websiteInfo.url.absoluteString, title: websiteInfo.title ?? "", isFavorite: false) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: WebsiteInfo(tab)), bookmarkManager: bookmarkManager) + + // WHEN + let name = sut.bookmarkName + let url = sut.bookmarkURLPath + + // THEN + XCTAssertEqual(name, "") + XCTAssertEqual(url, "") + } + func testShouldSetBookmarkNameToValueWhenInitAndModeIsEdit() { // GIVEN let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) @@ -178,7 +196,6 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { func testShouldSetSelectedFolderToNilWhenBookmarkParentFolderIsNilAndModeIsAdd() { // GIVEN let folder = BookmarkFolder(id: "1", title: #function) - let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "2") bookmarkStoreMock.bookmarks = [folder] bookmarkManager.loadBookmarks() let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) From fd2434e1c6eb8cc05f5b2dce4e58ae5455282db5 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 1 Mar 2024 18:28:30 +1100 Subject: [PATCH 07/19] Bookmarks bar and Favorites changes (#2286) Task/Issue URL: https://app.asana.com/0/0/1206713604816890/f **Description**: 1. Edit Bookmarks & Folders from Bookmarks Bar. 2. Add/Edit Bookmarks from Favorites view. 3. Fix an issue that caused bookmarks to move unnecessarily when already in the root Bookmarks folder --- .../Dialog/BookmarksDialogViewFactory.swift | 18 +++-- .../AddEditBookmarkDialogViewModel.swift | 34 +++++++-- .../View/BookmarksBarViewController.swift | 4 +- .../Model/HomePageFavoritesModel.swift | 15 ++-- DuckDuckGo/HomePage/View/FavoritesView.swift | 3 + .../View/HomePageViewController.swift | 19 +++-- .../AddEditBookmarkDialogViewModelTests.swift | 71 +++++++++++++++++++ 7 files changed, 138 insertions(+), 26 deletions(-) diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift index cffb72774e..b29b50bbbb 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarksDialogViewFactory.swift @@ -25,7 +25,7 @@ enum BookmarksDialogViewFactory { /// - Parameters: /// - parentFolder: An optional `BookmarkFolder`. When adding a folder to the root bookmark folder pass `nil`. For any other folder pass the `BookmarkFolder` the new folder should be within. /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. - /// - Returns: An instance of AddEditBookmarkFolderDialogView + /// - Returns: An instance of AddEditBookmarkFolderDialogView. static func makeAddBookmarkFolderView(parentFolder: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkFolderDialogView { let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: parentFolder), bookmarkManager: bookmarkManager) return AddEditBookmarkFolderDialogView(viewModel: viewModel) @@ -36,7 +36,7 @@ enum BookmarksDialogViewFactory { /// - folder: The `BookmarkFolder` to edit. /// - parentFolder: An optional `BookmarkFolder`. When editing a folder within the root bookmark folder pass `nil`. For any other folder pass the `BookmarkFolder` the folder belongs to. /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. - /// - Returns: An instance of AddEditBookmarkFolderDialogView + /// - Returns: An instance of AddEditBookmarkFolderDialogView. static func makeEditBookmarkFolderView(folder: BookmarkFolder, parentFolder: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkFolderDialogView { let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: parentFolder), bookmarkManager: bookmarkManager) return AddEditBookmarkFolderDialogView(viewModel: viewModel) @@ -46,7 +46,7 @@ enum BookmarksDialogViewFactory { /// - Parameters: /// - currentTab: An optional `WebsiteInfo`. When adding a bookmark from the bookmark shortcut panel, if the `Tab` has loaded a web page pass the information via the `currentTab`. If the `Tab` has not loaded a tab pass `nil`. If adding a `Bookmark` from the `Manage Bookmark` settings page, pass `nil`. /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. - /// - Returns: An instance of AddEditBookmarkDialogView + /// - Returns: An instance of AddEditBookmarkDialogView. static func makeAddBookmarkView(currentTab: WebsiteInfo?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { let viewModel = AddEditBookmarkDialogViewModel(mode: .add(tabWebsite: currentTab), bookmarkManager: bookmarkManager) return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) @@ -56,17 +56,25 @@ enum BookmarksDialogViewFactory { /// - Parameters: /// - parentFolder: An optional `BookmarkFolder`. When adding a bookmark from the bookmark management view, if the user select a parent folder pass this value won't be `nil`. Otherwise, if no folder is selected this value will be `nil`. /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. - /// - Returns: An instance of AddEditBookmarkDialogView + /// - Returns: An instance of AddEditBookmarkDialogView. static func makeAddBookmarkView(parent: BookmarkFolder?, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { let viewModel = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: parent), bookmarkManager: bookmarkManager) return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) } + /// Creates an instance of AddEditBookmarkDialogView for adding a Bookmark from the Favorites view in the empty Tab. + /// - Parameter bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. + /// - Returns: An instance of AddEditBookmarkDialogView, + static func makeAddFavoriteView(bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { + let viewModel = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: true), bookmarkManager: bookmarkManager) + return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) + } + /// Creates an instance of AddEditBookmarkDialogView for editing a Bookmark. /// - Parameters: /// - bookmark: The `Bookmark` to edit. /// - bookmarkManager: An instance of `BookmarkManager`. This should be used for `#previews` only. - /// - Returns: An instance of AddEditBookmarkDialogView + /// - Returns: An instance of AddEditBookmarkDialogView. static func makeEditBookmarkView(bookmark: Bookmark, bookmarkManager: LocalBookmarkManager = .shared) -> AddEditBookmarkDialogView { let viewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) return makeAddEditBookmarkDialogView(viewModel: viewModel, bookmarkManager: bookmarkManager) diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift index e435aa9f46..66620ee60a 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift @@ -37,7 +37,8 @@ final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { /// If the users add a bookmark to the root `Bookmarks` folder, then the parent folder is `nil`. /// If the users add a bookmark to a different folder then the parent folder is not `nil`. /// If the users add a bookmark from the bookmark shortcut and `Tab` has a page loaded, then the `tabWebsite` is not `nil`. - case add(tabWebsite: WebsiteInfo? = nil, parentFolder: BookmarkFolder? = nil) + /// When adding a bookmark from favorite screen the `shouldPresetFavorite` flag should be set to `true`. + case add(tabWebsite: WebsiteInfo? = nil, parentFolder: BookmarkFolder? = nil, shouldPresetFavorite: Bool = false) /// Edit an existing bookmark. case edit(bookmark: Bookmark) } @@ -78,12 +79,12 @@ final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { private let bookmarkManager: BookmarkManager init(mode: Mode, bookmarkManager: LocalBookmarkManager = .shared) { + let isFavorite = mode.bookmarkURL.flatMap(bookmarkManager.isUrlFavorited) ?? false self.mode = mode self.bookmarkManager = bookmarkManager - self.isBookmarkFavorite = mode.bookmarkURL.flatMap(bookmarkManager.isUrlFavorited) ?? false folders = .init(bookmarkManager.list) switch mode { - case let .add(websiteInfo, parentFolder): + case let .add(websiteInfo, parentFolder, shouldPresetFavorite): // When adding a new bookmark with website info we need to show the bookmark name and URL only if the bookmark is not bookmarked already. // Scenario we click on the "Add Bookmark" button from Bookmarks shortcut Panel. If Tab has a Bookmark loaded we present the dialog with prepopulated name and URL from the tab. // If we save and click again on the "Add Bookmark" button we don't want to try re-add the same bookmark. Hence we present a dialog that is not pre-populated. @@ -92,10 +93,12 @@ final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { let websiteURLPath = isAlreadyBookmarked ? "" : websiteInfo?.url.absoluteString ?? "" bookmarkName = websiteName bookmarkURLPath = websiteURLPath + isBookmarkFavorite = shouldPresetFavorite ? true : isFavorite selectedFolder = parentFolder case let .edit(bookmark): bookmarkName = bookmark.title bookmarkURLPath = bookmark.urlObject?.absoluteString ?? "" + isBookmarkFavorite = isFavorite selectedFolder = folders.first(where: { $0.id == bookmark.parentFolderUUID })?.entity } @@ -148,9 +151,9 @@ private extension AddEditBookmarkDialogViewModel { bookmarkManager.update(bookmark: bookmark) } // If the bookmark changed parent location, move it. - if bookmark.parentFolderUUID != selectedFolder?.id { - let parentFoler: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root - bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: parentFoler, completion: { _ in }) + if shouldMove(bookmark: bookmark) { + let parentFolder: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: parentFolder, completion: { _ in }) } } @@ -162,6 +165,15 @@ private extension AddEditBookmarkDialogViewModel { bookmarkManager.makeBookmark(for: url, title: name, isFavorite: isFavorite, index: nil, parent: parent) } } + + func shouldMove(bookmark: Bookmark) -> Bool { + // There's a discrepancy in representing the root folder. A bookmark belonging to the root folder has `parentFolderUUID` equal to `bookmarks_root`. + // There's no `BookmarkFolder` to represent the root folder, so the root folder is represented by a nil selectedFolder. + // Move Bookmarks if its parent folder is != from the selected folder but ONLY if: + // - The selected folder is not nil. This ensure we're comparing a subfolder with any bookmark parent folder. + // - The selected folder is nil and the bookmark parent folder is not the root folder. This ensure we're not unnecessarily moving the items within the same root folder. + bookmark.parentFolderUUID != selectedFolder?.id && (selectedFolder != nil || selectedFolder == nil && !bookmark.isParentFolderRoot) + } } private extension AddEditBookmarkDialogViewModel.Mode { @@ -193,7 +205,7 @@ private extension AddEditBookmarkDialogViewModel.Mode { var bookmarkURL: URL? { switch self { - case let .add(tabInfo, _): + case let .add(tabInfo, _, _): return tabInfo?.url case let .edit(bookmark): return bookmark.urlObject @@ -201,3 +213,11 @@ private extension AddEditBookmarkDialogViewModel.Mode { } } + +private extension Bookmark { + + var isParentFolderRoot: Bool { + parentFolderUUID == "bookmarks_root" + } + +} diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift index ea9b61e496..7782688531 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift @@ -237,7 +237,7 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { bookmark.isFavorite = true bookmarkManager.update(bookmark: bookmark) case .edit: - AddBookmarkModalView(model: AddBookmarkModalViewModel(originalBookmark: bookmark)) + BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) .show(in: view.window) case .moveToEnd: bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: .root) { _ in } @@ -258,7 +258,7 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { menu.popUp(positioning: nil, at: CGPoint(x: 0, y: item.view.frame.minY - 7), in: item.view) case .edit: - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: nil) .show(in: view.window) case .moveToEnd: bookmarkManager.move(objectUUIDs: [folder.id], toIndex: nil, withinParentFolder: .root) { _ in } diff --git a/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift b/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift index 0860f02cfc..e88a48b1b0 100644 --- a/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageFavoritesModel.swift @@ -86,14 +86,16 @@ extension HomePage.Models { let open: (Bookmark, OpenTarget) -> Void let removeFavorite: (Bookmark) -> Void let deleteBookmark: (Bookmark) -> Void - let addEdit: (Bookmark?) -> Void + let add: () -> Void + let edit: (Bookmark) -> Void let moveFavorite: (Bookmark, Int) -> Void let onFaviconMissing: () -> Void init(open: @escaping (Bookmark, OpenTarget) -> Void, removeFavorite: @escaping (Bookmark) -> Void, deleteBookmark: @escaping (Bookmark) -> Void, - addEdit: @escaping (Bookmark?) -> Void, + add: @escaping () -> Void, + edit: @escaping (Bookmark) -> Void, moveFavorite: @escaping (Bookmark, Int) -> Void, onFaviconMissing: @escaping () -> Void ) { @@ -102,7 +104,8 @@ extension HomePage.Models { self.open = open self.removeFavorite = removeFavorite self.deleteBookmark = deleteBookmark - self.addEdit = addEdit + self.add = add + self.edit = edit self.moveFavorite = moveFavorite self.onFaviconMissing = onFaviconMissing } @@ -119,12 +122,12 @@ extension HomePage.Models { open(bookmark, .current) } - func edit(_ bookmark: Bookmark) { - addEdit(bookmark) + func editBookmark(_ bookmark: Bookmark) { + edit(bookmark) } func addNew() { - addEdit(nil) + add() } private func updateVisibleModels() { diff --git a/DuckDuckGo/HomePage/View/FavoritesView.swift b/DuckDuckGo/HomePage/View/FavoritesView.swift index 90956a4a66..52e7f585d1 100644 --- a/DuckDuckGo/HomePage/View/FavoritesView.swift +++ b/DuckDuckGo/HomePage/View/FavoritesView.swift @@ -305,14 +305,17 @@ struct Favorite: View { let bookmark: Bookmark // Maintain separate copies of bookmark metadata required by the view, in order to ensure that SwiftUI re-renders correctly. + // Do not remove these properties even if some are not used in the `FavoriteTemplate` view as the view will not re-render correctly. private let bookmarkTitle: String private let bookmarkURL: URL + private let bookmarkParentFolder: String? init?(bookmark: Bookmark) { guard let urlObject = bookmark.urlObject else { return nil } self.bookmark = bookmark self.bookmarkTitle = bookmark.title self.bookmarkURL = urlObject + self.bookmarkParentFolder = bookmark.parentFolderUUID } var body: some View { diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index ae40aff5df..03f2cb4e08 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -172,8 +172,10 @@ final class HomePageViewController: NSViewController { self?.bookmarkManager.update(bookmark: bookmark) }, deleteBookmark: { [weak self] bookmark in self?.bookmarkManager.remove(bookmark: bookmark) - }, addEdit: { [weak self] bookmark in - self?.showAddEditController(for: bookmark) + }, add: { [weak self] in + self?.showAddController() + }, edit: { [weak self] bookmark in + self?.showEditController(for: bookmark) }, moveFavorite: { [weak self] (bookmark, index) in self?.bookmarkManager.moveFavorites(with: [bookmark.id], toIndex: index) { _ in } }, onFaviconMissing: { [weak self] in @@ -204,7 +206,7 @@ final class HomePageViewController: NSViewController { } func subscribeToBookmarks() { - bookmarkManager.listPublisher.receive(on: RunLoop.main).sink { [weak self] _ in + bookmarkManager.listPublisher.receive(on: DispatchQueue.main).sink { [weak self] _ in withAnimation { self?.refreshFavoritesModel() } @@ -230,9 +232,14 @@ final class HomePageViewController: NSViewController { tabCollectionViewModel.selectedTabViewModel?.tab.setContent(.contentFromURL(url, source: .bookmark)) } - private func showAddEditController(for bookmark: Bookmark? = nil) { - AddBookmarkModalView(model: AddBookmarkModalViewModel(originalBookmark: bookmark, isFavorite: true)) - .show(in: self.view.window) + private func showAddController() { + BookmarksDialogViewFactory.makeAddFavoriteView() + .show(in: view.window) + } + + private func showEditController(for bookmark: Bookmark) { + BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) + .show(in: view.window) } private var burningDataCancellable: AnyCancellable? diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift index 18ad304a8c..d564fb4064 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -251,6 +251,28 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertEqual(result, folder) } + func testShouldSetIsBookmarkFavoriteToTrueWhenModeIsAddAndShouldPresetFavoriteIsTrue() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: true), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isBookmarkFavorite + + // THEN + XCTAssertTrue(result) + } + + func testShouldNotSetIsBookmarkFavoriteToTrueWhenModeIsAddAndShouldPresetFavoriteIsFalse() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(shouldPresetFavorite: false), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isBookmarkFavorite + + // THEN + XCTAssertFalse(result) + } + // MARK: - Actions func testReturnIsCancelActionDisabledFalseWhenModeIsAdd() { @@ -622,6 +644,31 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: folder.id)) } + func testShouldAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNilAndOriginalFolderIsNotRootFolderAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "ABCDE") + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder", children: [bookmark]) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [folder, bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [bookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .root) + } + func testShouldNotAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNotDifferentFromOriginalFolderAndModeIsEdit() { // GIVEN let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "ABCDE") @@ -646,4 +693,28 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) } + + func testShouldNotAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNilAndOriginalFolderIsRootAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "bookmarks_root") + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = nil + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } } From 7aa846367a44ccf38cb92084be12e8d11bcd61b7 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 5 Mar 2024 15:58:29 +1100 Subject: [PATCH 08/19] Bookmarks Add delete icons on multiple items selection (#2271) Task/Issue URL: https://app.asana.com/0/0/1206531304671953/f **Description**: 1. Show / Hide Delete buttons when multiple/single items get selected. 2. Clean up duplicate code for setting up UI and layout. --- .../Images/Trash.imageset/Contents.json | 15 ++ .../Images/Trash.imageset/Trash.svg | 9 + ...okmarkManagementDetailViewController.swift | 219 ++++++++++-------- 3 files changed, 148 insertions(+), 95 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg diff --git a/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json new file mode 100644 index 0000000000..ee8f9e1708 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Trash.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg new file mode 100644 index 0000000000..385873ffbf --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Trash.imageset/Trash.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index a996cd2bf7..cf16261672 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -32,8 +32,10 @@ private struct EditedBookmarkMetadata { final class BookmarkManagementDetailViewController: NSViewController, NSMenuItemValidation { + private let toolbarButtonsStackView = NSStackView() private lazy var newBookmarkButton = MouseOverButton(title: " " + UserText.newBookmark, target: self, action: #selector(presentAddBookmarkModal)) private lazy var newFolderButton = MouseOverButton(title: " " + UserText.newFolder, target: self, action: #selector(presentAddFolderModal)) + private lazy var deleteItemsButton = MouseOverButton(title: " " + UserText.bookmarksBarContextMenuDelete, target: self, action: #selector(delete)) private lazy var separator = NSBox() private lazy var scrollView = NSScrollView() @@ -75,34 +77,16 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem view.addSubview(separator) view.addSubview(scrollView) view.addSubview(emptyState) - view.addSubview(newBookmarkButton) - view.addSubview(newFolderButton) - - newBookmarkButton.bezelStyle = .shadowlessSquare - newBookmarkButton.cornerRadius = 4 - newBookmarkButton.normalTintColor = .button - newBookmarkButton.mouseDownColor = .buttonMouseDown - newBookmarkButton.mouseOverColor = .buttonMouseOver - newBookmarkButton.imageHugsTitle = true - newBookmarkButton.setContentHuggingPriority(.defaultHigh, for: .vertical) - newBookmarkButton.translatesAutoresizingMaskIntoConstraints = false - newBookmarkButton.alignment = .center - newBookmarkButton.font = .systemFont(ofSize: 13) - newBookmarkButton.image = .addBookmark - newBookmarkButton.imagePosition = .imageLeading - - newFolderButton.bezelStyle = .shadowlessSquare - newFolderButton.cornerRadius = 4 - newFolderButton.normalTintColor = .button - newFolderButton.mouseDownColor = .buttonMouseDown - newFolderButton.mouseOverColor = .buttonMouseOver - newFolderButton.imageHugsTitle = true - newFolderButton.setContentHuggingPriority(.defaultHigh, for: .vertical) - newFolderButton.translatesAutoresizingMaskIntoConstraints = false - newFolderButton.alignment = .center - newFolderButton.font = .systemFont(ofSize: 13) - newFolderButton.image = .addFolder - newFolderButton.imagePosition = .imageLeading + view.addSubview(toolbarButtonsStackView) + toolbarButtonsStackView.addArrangedSubview(newBookmarkButton) + toolbarButtonsStackView.addArrangedSubview(newFolderButton) + toolbarButtonsStackView.addArrangedSubview(deleteItemsButton) + toolbarButtonsStackView.translatesAutoresizingMaskIntoConstraints = false + toolbarButtonsStackView.distribution = .fill + + configureToolbar(button: newBookmarkButton, image: .addBookmark, isHidden: false) + configureToolbar(button: newFolderButton, image: .addFolder, isHidden: false) + configureToolbar(button: deleteItemsButton, image: .trash, isHidden: true) emptyState.addSubview(emptyStateImageView) emptyState.addSubview(emptyStateTitle) @@ -111,32 +95,27 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem emptyState.isHidden = true emptyState.translatesAutoresizingMaskIntoConstraints = false + importButton.translatesAutoresizingMaskIntoConstraints = false - emptyStateTitle.isEditable = false - emptyStateTitle.setContentHuggingPriority(.defaultHigh, for: .vertical) - emptyStateTitle.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - emptyStateTitle.translatesAutoresizingMaskIntoConstraints = false - emptyStateTitle.alignment = .center - emptyStateTitle.drawsBackground = false - emptyStateTitle.isBordered = false - emptyStateTitle.font = .systemFont(ofSize: 15, weight: .semibold) - emptyStateTitle.textColor = .labelColor - emptyStateTitle.attributedStringValue = NSAttributedString.make(UserText.bookmarksEmptyStateTitle, - lineHeight: 1.14, - kern: -0.23) - - emptyStateMessage.isEditable = false - emptyStateMessage.setContentHuggingPriority(.defaultHigh, for: .vertical) - emptyStateMessage.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) - emptyStateMessage.translatesAutoresizingMaskIntoConstraints = false - emptyStateMessage.alignment = .center - emptyStateMessage.drawsBackground = false - emptyStateMessage.isBordered = false - emptyStateMessage.font = .systemFont(ofSize: 13) - emptyStateMessage.textColor = .labelColor - emptyStateMessage.attributedStringValue = NSAttributedString.make(UserText.bookmarksEmptyStateMessage, - lineHeight: 1.05, - kern: -0.08) + configureEmptyState( + label: emptyStateTitle, + font: .systemFont(ofSize: 15, weight: .semibold), + attributedTitle: .make( + UserText.bookmarksEmptyStateTitle, + lineHeight: 1.14, + kern: -0.23 + ) + ) + + configureEmptyState( + label: emptyStateMessage, + font: .systemFont(ofSize: 13), + attributedTitle: .make( + UserText.bookmarksEmptyStateMessage, + lineHeight: 1.05, + kern: -0.08 + ) + ) emptyStateImageView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) emptyStateImageView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) @@ -182,47 +161,47 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem } private func setupLayout() { - newBookmarkButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48).isActive = true - view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 48).isActive = true - separator.topAnchor.constraint(equalTo: newBookmarkButton.bottomAnchor, constant: 24).isActive = true - emptyState.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 20).isActive = true - scrollView.topAnchor.constraint(equalTo: separator.bottomAnchor).isActive = true - - view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true - view.trailingAnchor.constraint(greaterThanOrEqualTo: newFolderButton.trailingAnchor, constant: 20).isActive = true - view.trailingAnchor.constraint(equalTo: separator.trailingAnchor, constant: 58).isActive = true - newFolderButton.leadingAnchor.constraint(equalTo: newBookmarkButton.trailingAnchor, constant: 16).isActive = true - emptyState.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true - newFolderButton.centerYAnchor.constraint(equalTo: newBookmarkButton.centerYAnchor).isActive = true - separator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 58).isActive = true - newBookmarkButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32).isActive = true - emptyState.topAnchor.constraint(greaterThanOrEqualTo: separator.bottomAnchor, constant: 8).isActive = true - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48).isActive = true - emptyState.centerXAnchor.constraint(equalTo: separator.centerXAnchor).isActive = true - - newBookmarkButton.heightAnchor.constraint(equalToConstant: 24).isActive = true - - newFolderButton.heightAnchor.constraint(equalToConstant: 24).isActive = true - - emptyStateMessage.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true + NSLayoutConstraint.activate([ + toolbarButtonsStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48), + view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 48), + separator.topAnchor.constraint(equalTo: toolbarButtonsStackView.bottomAnchor, constant: 24), + emptyState.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 20), + scrollView.topAnchor.constraint(equalTo: separator.bottomAnchor), - importButton.translatesAutoresizingMaskIntoConstraints = false - importButton.topAnchor.constraint(equalTo: emptyStateMessage.bottomAnchor, constant: 8).isActive = true - emptyState.heightAnchor.constraint(equalToConstant: 218).isActive = true - emptyStateMessage.topAnchor.constraint(equalTo: emptyStateTitle.bottomAnchor, constant: 8).isActive = true - importButton.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyStateImageView.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyState.widthAnchor.constraint(equalToConstant: 224).isActive = true - emptyStateImageView.topAnchor.constraint(equalTo: emptyState.topAnchor).isActive = true - emptyStateTitle.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor).isActive = true - emptyStateTitle.topAnchor.constraint(equalTo: emptyStateImageView.bottomAnchor, constant: 8).isActive = true + view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + view.trailingAnchor.constraint(greaterThanOrEqualTo: toolbarButtonsStackView.trailingAnchor, constant: 20), + view.trailingAnchor.constraint(equalTo: separator.trailingAnchor, constant: 58), + emptyState.centerXAnchor.constraint(equalTo: view.centerXAnchor), + separator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 58), + toolbarButtonsStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 32), + emptyState.topAnchor.constraint(greaterThanOrEqualTo: separator.bottomAnchor, constant: 8), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 48), + emptyState.centerXAnchor.constraint(equalTo: separator.centerXAnchor), + + newBookmarkButton.heightAnchor.constraint(equalToConstant: 24), + newFolderButton.heightAnchor.constraint(equalToConstant: 24), + deleteItemsButton.heightAnchor.constraint(equalToConstant: 24), + + emptyStateMessage.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), - emptyStateMessage.widthAnchor.constraint(equalToConstant: 192).isActive = true + importButton.topAnchor.constraint(equalTo: emptyStateMessage.bottomAnchor, constant: 8), + emptyState.heightAnchor.constraint(equalToConstant: 218), + emptyStateMessage.topAnchor.constraint(equalTo: emptyStateTitle.bottomAnchor, constant: 8), + importButton.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyStateImageView.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyState.widthAnchor.constraint(equalToConstant: 224), + emptyStateImageView.topAnchor.constraint(equalTo: emptyState.topAnchor), + emptyStateTitle.centerXAnchor.constraint(equalTo: emptyState.centerXAnchor), + emptyStateTitle.topAnchor.constraint(equalTo: emptyStateImageView.bottomAnchor, constant: 8), - emptyStateTitle.widthAnchor.constraint(equalToConstant: 192).isActive = true + emptyStateMessage.widthAnchor.constraint(equalToConstant: 192), + + emptyStateTitle.widthAnchor.constraint(equalToConstant: 192), + + emptyStateImageView.widthAnchor.constraint(equalToConstant: 128), + emptyStateImageView.heightAnchor.constraint(equalToConstant: 96), + ]) - emptyStateImageView.widthAnchor.constraint(equalToConstant: 128).isActive = true - emptyStateImageView.heightAnchor.constraint(equalToConstant: 96).isActive = true } override func viewDidLoad() { @@ -252,6 +231,8 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem let scrollPosition = tableView.visibleRect.origin tableView.reloadData() tableView.scroll(scrollPosition) + + updateToolbarButtons() } @objc func onImportClicked(_ sender: NSButton) { @@ -521,11 +502,25 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } func onSelectionChanged() { - resetSelections() - let indexes = tableView.selectedRowIndexes - indexes.forEach { - let cell = self.tableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? BookmarkTableCellView - cell?.isSelected = true + func updateCellSelections() { + resetSelections() + tableView.selectedRowIndexes.forEach { + let cell = self.tableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? BookmarkTableCellView + cell?.isSelected = true + } + } + + updateCellSelections() + updateToolbarButtons() + } + + private func updateToolbarButtons() { + let shouldShowDeleteButton = tableView.selectedRowIndexes.count > 1 + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.25 + deleteItemsButton.animator().isHidden = !shouldShowDeleteButton + newBookmarkButton.animator().isHidden = shouldShowDeleteButton + newFolderButton.animator().isHidden = shouldShowDeleteButton } } @@ -544,6 +539,40 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi } } +// MARK: - Private + +private extension BookmarkManagementDetailViewController { + + func configureToolbar(button: MouseOverButton, image: NSImage, isHidden: Bool) { + button.bezelStyle = .shadowlessSquare + button.cornerRadius = 4 + button.normalTintColor = .button + button.mouseDownColor = .buttonMouseDown + button.mouseOverColor = .buttonMouseOver + button.imageHugsTitle = true + button.setContentHuggingPriority(.defaultHigh, for: .vertical) + button.alignment = .center + button.font = .systemFont(ofSize: 13) + button.image = image + button.imagePosition = .imageLeading + button.isHidden = isHidden + } + + func configureEmptyState(label: NSTextField, font: NSFont, attributedTitle: NSAttributedString) { + label.isEditable = false + label.setContentHuggingPriority(.defaultHigh, for: .vertical) + label.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + label.translatesAutoresizingMaskIntoConstraints = false + label.alignment = .center + label.drawsBackground = false + label.isBordered = false + label.font = font + label.textColor = .labelColor + label.attributedStringValue = attributedTitle + } + +} + // MARK: - BookmarkTableCellViewDelegate extension BookmarkManagementDetailViewController: BookmarkTableCellViewDelegate { From 6662df79dccf6591f9af1d622663befc7f505f1e Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 7 Mar 2024 11:07:13 +1100 Subject: [PATCH 09/19] Centralise Contextual Menu (#2297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206713604816893/f **Description**: 1. Centralise contextual menu creation for bookmarks and folders as the menu should be the same no matter where we present it from. 2. Show the same contextual menu for bookmarks from the `Bookmarks Shortcut panel`, `Manage Bookmarks` and `Bookmarks Bar`. 3. Show the same contextual menu for folders from the `Bookmarks Shortcut panel`, `Manage Bookmarks` and `Bookmarks Bar`. 4. Add “Open All in New Window” to the folder menu. 5. Add “Add Folder”, “Manage Bookmarks” and “Move to End” to all the menus. --- DuckDuckGo.xcodeproj/project.pbxproj | 36 +++ .../Bookmarks/Extensions/Bookmarks+Tab.swift | 41 +++ .../Bookmarks/Model/BookmarkFolderInfo.swift | 32 +++ .../Bookmarks/Services/ContextualMenu.swift | 201 ++++++------- .../Services/MenuItemSelectors.swift | 13 +- .../View/BookmarkListViewController.swift | 61 ++-- ...okmarkManagementDetailViewController.swift | 41 ++- ...kmarkManagementSidebarViewController.swift | 47 ++- .../View/BookmarksBarCollectionViewItem.swift | 108 +++---- .../View/BookmarksBarViewController.swift | 84 ++++-- .../View/BookmarksBarViewModel.swift | 16 +- .../Extensions/Bookmarks+TabTests.swift | 61 ++++ .../Bookmarks/Model/ContextualMenuTests.swift | 267 ++++++++++++++++++ .../BookmarksBarViewModelTests.swift | 214 ++++++++++++++ 14 files changed, 978 insertions(+), 244 deletions(-) create mode 100644 DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift create mode 100644 DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift create mode 100644 UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift create mode 100644 UnitTests/Bookmarks/Model/ContextualMenuTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0910958a60..8057dec63c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2433,6 +2433,16 @@ 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; 9F56CFB32B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; + 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D9A2B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; + 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; + 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */; }; + 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */; }; + 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */; }; + 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; + 9F872DA52B90920F00138637 /* BookmarkFolderInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */; }; 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; @@ -4046,6 +4056,10 @@ 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; + 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+Tab.swift"; sourceTree = ""; }; + 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; + 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenuTests.swift; sourceTree = ""; }; + 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFolderInfo.swift; sourceTree = ""; }; 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; @@ -5892,6 +5906,7 @@ children = ( 4B9292AE26670F5300AD2C21 /* NSOutlineViewExtensions.swift */, B6C0BB6629AEFF8100AE8E3C /* BookmarkExtension.swift */, + 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */, ); path = Extensions; sourceTree = ""; @@ -6713,6 +6728,14 @@ path = DuckDuckGoDBPBackgroundAgent; sourceTree = ""; }; + 9F872D9B2B9058B000138637 /* Extensions */ = { + isa = PBXGroup; + children = ( + 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 9F982F102B82264400231028 /* ViewModels */ = { isa = PBXGroup; children = ( @@ -7087,6 +7110,7 @@ AA652CAB25DD820D009059CC /* Bookmarks */ = { isa = PBXGroup; children = ( + 9F872D9B2B9058B000138637 /* Extensions */, 9F982F102B82264400231028 /* ViewModels */, AA652CAE25DD8228009059CC /* Model */, AA652CAF25DD822C009059CC /* Services */, @@ -7109,6 +7133,7 @@ AA652CCD25DD9071009059CC /* BookmarkListTests.swift */, AA652CD225DDA6E9009059CC /* LocalBookmarkManagerTests.swift */, 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */, + 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */, ); path = Model; sourceTree = ""; @@ -7611,6 +7636,7 @@ AAC5E4CE25D6A709007F5990 /* BookmarkManager.swift */, 379E877529E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift */, B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */, + 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */, ); path = Model; sourceTree = ""; @@ -10451,6 +10477,7 @@ 3706FC54293F65D500E42796 /* BookmarkListTreeControllerDataSource.swift in Sources */, 3706FC55293F65D500E42796 /* AddressBarViewController.swift in Sources */, 3706FC56293F65D500E42796 /* Permissions.swift in Sources */, + 9F872D992B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, B6B4D1D02B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, 3706FC57293F65D500E42796 /* TabPreviewWindowController.swift in Sources */, 3706FC58293F65D500E42796 /* NSSizeExtension.swift in Sources */, @@ -10475,6 +10502,7 @@ 3706FC65293F65D500E42796 /* HomePageViewController.swift in Sources */, 3706FC67293F65D500E42796 /* OperatingSystemVersionExtension.swift in Sources */, B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */, + 9F872DA42B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, B677FC502B06376B0099EB04 /* ReportFeedbackView.swift in Sources */, 3706FC68293F65D500E42796 /* ToggleableScrollView.swift in Sources */, 3706FC69293F65D500E42796 /* UserScripts.swift in Sources */, @@ -10598,6 +10626,7 @@ 3706FDF8293F661700E42796 /* FileStoreTests.swift in Sources */, 5603D90729B7B746007F9F01 /* MockTabViewItemDelegate.swift in Sources */, 3706FDF9293F661700E42796 /* TabViewModelTests.swift in Sources */, + 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */, 3706FDFA293F661700E42796 /* DefaultBrowserPreferencesTests.swift in Sources */, 3706FDFB293F661700E42796 /* DispatchQueueExtensionsTests.swift in Sources */, 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */, @@ -10749,6 +10778,7 @@ 3706FE64293F661700E42796 /* DownloadListStoreTests.swift in Sources */, 3706FE65293F661700E42796 /* ContentBlockingUpdatingTests.swift in Sources */, 3706FE67293F661700E42796 /* EncryptionMocks.swift in Sources */, + 9F872D9E2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 3706FE68293F661700E42796 /* DuckPlayerURLExtensionTests.swift in Sources */, 3706FE6A293F661700E42796 /* FirefoxKeyReaderTests.swift in Sources */, 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, @@ -11040,6 +11070,7 @@ 4B95795D2AC7AE700062CA31 /* OptionalExtension.swift in Sources */, 4B95795E2AC7AE700062CA31 /* PasswordManagementLoginItemView.swift in Sources */, 4B95795F2AC7AE700062CA31 /* UserText.swift in Sources */, + 9F872D9A2B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, 4B9579602AC7AE700062CA31 /* WKWebView+Download.swift in Sources */, 4B9579612AC7AE700062CA31 /* TabShadowConfig.swift in Sources */, 4B9579622AC7AE700062CA31 /* URLSessionExtension.swift in Sources */, @@ -11060,6 +11091,7 @@ 4B95796E2AC7AE700062CA31 /* LegacyBookmarkStore.swift in Sources */, 4B95796F2AC7AE700062CA31 /* NSAlert+DataImport.swift in Sources */, 4B9579702AC7AE700062CA31 /* MainWindow.swift in Sources */, + 9F872DA52B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 4B9579712AC7AE700062CA31 /* CrashReportPromptViewController.swift in Sources */, 4B9579722AC7AE700062CA31 /* BookmarksCleanupErrorHandling.swift in Sources */, @@ -11972,6 +12004,7 @@ AA92127725ADA07900600CD4 /* WKWebViewExtension.swift in Sources */, AAAB9114288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift in Sources */, B6C0B23626E732000031CB7F /* DownloadListItem.swift in Sources */, + 9F872DA32B90920F00138637 /* BookmarkFolderInfo.swift in Sources */, 4B9DB0232A983B24000927DB /* WaitlistRequest.swift in Sources */, B6B1E87E26D5DA0E0062C350 /* DownloadsPopover.swift in Sources */, 85774AFF2A713D3B00DE0561 /* BookmarksBarMenuFactory.swift in Sources */, @@ -12233,6 +12266,7 @@ 1DB67F2D2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift in Sources */, 4B44FEF32B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */, 4B59023E26B35F3600489384 /* ChromiumLoginReader.swift in Sources */, + 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */, 983DFB2528B67036006B7E34 /* UserContentUpdating.swift in Sources */, 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, @@ -12648,12 +12682,14 @@ B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */, 37CD54BB27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift in Sources */, 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, + 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */, 4B9DB0562A983B55000927DB /* MockNotificationService.swift in Sources */, 4B02199C25E063DE00ED7DEA /* FireproofDomainsTests.swift in Sources */, AA0F3DB7261A566C0077F2D9 /* SuggestionLoadingMock.swift in Sources */, B60C6F8129B1B4AD007BFAA8 /* TestRunHelper.swift in Sources */, 4B9DB0582A983B55000927DB /* MockNetworkProtectionCodeRedeemer.swift in Sources */, + 9F872DA02B90644800138637 /* ContextualMenuTests.swift in Sources */, 4B9292BE2667103100AD2C21 /* PasteboardFolderTests.swift in Sources */, 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, 4B723E1926B000DC00E14D75 /* TemporaryFileCreator.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift b/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift new file mode 100644 index 0000000000..82e5748c72 --- /dev/null +++ b/DuckDuckGo/Bookmarks/Extensions/Bookmarks+Tab.swift @@ -0,0 +1,41 @@ +// +// Bookmarks+Tab.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension Tab { + + @MainActor + static func withContentOfBookmark(folder: BookmarkFolder, burnerMode: BurnerMode) -> [Tab] { + folder.children.compactMap { entity in + guard let url = (entity as? Bookmark)?.urlObject else { return nil } + return Tab(content: .url(url, source: .bookmark), shouldLoadInBackground: true, burnerMode: burnerMode) + } + } + +} + +extension TabCollection { + + @MainActor + static func withContentOfBookmark(folder: BookmarkFolder, burnerMode: BurnerMode) -> TabCollection { + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: burnerMode) + return TabCollection(tabs: tabs) + } + +} diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift b/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift new file mode 100644 index 0000000000..a4f8004f71 --- /dev/null +++ b/DuckDuckGo/Bookmarks/Model/BookmarkFolderInfo.swift @@ -0,0 +1,32 @@ +// +// BookmarkFolderInfo.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol BookmarksEntityIdentifiable { + var entityId: String { get } + var parentId: String? { get } +} + +struct BookmarkEntityInfo: Equatable, BookmarksEntityIdentifiable { + let entity: BaseBookmarkEntity + let parent: BookmarkFolder? + + var entityId: String { entity.id } + var parentId: String? { parent?.id } +} diff --git a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift index ff4426c209..8447acac0a 100644 --- a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift @@ -18,20 +18,18 @@ import AppKit -struct ContextualMenu { +enum ContextualMenu { - // Not all contexts support an editing option for bookmarks. The option is displayed by default, but `includeBookmarkEditMenu` can disable it. - static func menu(for objects: [Any]?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { - menu(for: objects, target: nil, includeBookmarkEditMenu: includeBookmarkEditMenu) + static func menu(for objects: [Any]?) -> NSMenu? { + menu(for: objects, target: nil) } /// Creates an instance of NSMenu for the specified Objects and target. /// - Parameters: /// - objects: The objects to create the menu for. /// - target: The target to associate to the `NSMenuItem` - /// - includeBookmarkEditMenu: True if menu should return edit bookmark option. False otherwise. Default is true. /// - Returns: An instance of NSMenu or nil if `objects` is not a `Bookmark` or a `Folder`. - static func menu(for objects: [Any]?, target: AnyObject?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { + static func menu(for objects: [Any]?, target: AnyObject?) -> NSMenu? { guard let objects = objects, objects.count > 0 else { return menuForNoSelection() @@ -47,7 +45,7 @@ struct ContextualMenu { guard let object else { return nil } - let menu = menu(for: object, parentFolder: parentFolder, includeBookmarkEditMenu: includeBookmarkEditMenu) + let menu = menu(for: object, parentFolder: parentFolder) menu?.items.forEach { item in item.target = target @@ -61,12 +59,11 @@ struct ContextualMenu { /// - Parameters: /// - entity: The bookmark entity to create the menu for. /// - parentFolder: An optional `BookmarkFolder`. - /// - includeBookmarkEditMenu: True if menu should return edit bookmark option. False otherwise. Default is true. /// - Returns: An instance of NSMenu or nil if `entity` is not a `Bookmark` or a `Folder`. - static func menu(for entity: BaseBookmarkEntity, parentFolder: BookmarkFolder?, includeBookmarkEditMenu: Bool = true) -> NSMenu? { + static func menu(for entity: BaseBookmarkEntity, parentFolder: BookmarkFolder?) -> NSMenu? { let menu: NSMenu? if let bookmark = entity as? Bookmark { - menu = self.menu(for: bookmark, includeBookmarkEditMenu: includeBookmarkEditMenu) + menu = self.menu(for: bookmark, parent: parentFolder, isFavorite: bookmark.isFavorite) } else if let folder = entity as? BookmarkFolder { // When the user edits a folder we need to show the parent in the folder picker. Folders directly child of PseudoFolder `Bookmarks` have nil parent because their parent is not an instance of `BookmarkFolder` menu = self.menu(for: folder, parent: parentFolder) @@ -77,134 +74,149 @@ struct ContextualMenu { return menu } - // MARK: - Single Item Menu Creation - - private static func menuForNoSelection() -> NSMenu { - let menu = NSMenu(title: "") - menu.addItem(newFolderMenuItem()) - - return menu + /// Returns an array of `NSMenuItem` to show for a bookmark. + /// + /// - Important: The `representedObject` for the `NSMenuItem` returned is `nil`. This function is meant to be used for scenarios where the model is not available at the time of creating the `NSMenu` such as from the BookmarkBarCollectionViewItem. + /// + /// - Parameter isFavorite: True if the menu item should contain a menu item to add to favorites. False to contain a menu item to remove from favorites. + /// - Returns: An array of `NSMenuItem` + static func bookmarkMenuItems(isFavorite: Bool) -> [NSMenuItem] { + menuItems(for: nil, parent: nil, isFavorite: isFavorite) } - private static func menu(for bookmark: Bookmark, includeBookmarkEditMenu: Bool) -> NSMenu { - let menu = NSMenu(title: "") - - menu.addItem(openBookmarkInNewTabMenuItem(bookmark: bookmark)) - menu.addItem(openBookmarkInNewWindowMenuItem(bookmark: bookmark)) - menu.addItem(NSMenuItem.separator()) - - menu.addItem(addBookmarkToFavoritesMenuItem(bookmark: bookmark)) - - if includeBookmarkEditMenu { - menu.addItem(editBookmarkMenuItem(bookmark: bookmark)) - } + /// Returns an array of `NSMenuItem` to show for a bookmark folder. + /// + /// - Important: The `representedObject` for the `NSMenuItem` returned is `nil`. This function is meant to be used for scenarios where the model is not available at the time of creating the `NSMenu` such as from the BookmarkBarCollectionViewItem. + /// + /// - Returns: An array of `NSMenuItem` + static func folderMenuItems() -> [NSMenuItem] { + menuItems(for: nil, parent: nil) + } - menu.addItem(NSMenuItem.separator()) +} - menu.addItem(copyBookmarkMenuItem(bookmark: bookmark)) - menu.addItem(deleteBookmarkMenuItem(bookmark: bookmark)) - menu.addItem(NSMenuItem.separator()) +private extension ContextualMenu { - menu.addItem(newFolderMenuItem()) + static func menuForNoSelection() -> NSMenu { + NSMenu(items: [addFolderMenuItem(folder: nil)]) + } - return menu + static func menu(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool) -> NSMenu { + NSMenu(items: menuItems(for: bookmark, parent: parent, isFavorite: isFavorite)) } - private static func menu(for folder: BookmarkFolder, parent: BookmarkFolder?) -> NSMenu { - let menu = NSMenu(title: "") + static func menu(for folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenu { + NSMenu(items: menuItems(for: folder, parent: parent)) + } - menu.addItem(renameFolderMenuItem(folder: folder)) - menu.addItem(editFolderMenuItem(folder: folder, parent: parent)) - menu.addItem(deleteFolderMenuItem(folder: folder)) - menu.addItem(NSMenuItem.separator()) + static func menuItems(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool) -> [NSMenuItem] { + [ + openBookmarkInNewTabMenuItem(bookmark: bookmark), + openBookmarkInNewWindowMenuItem(bookmark: bookmark), + NSMenuItem.separator(), + addBookmarkToFavoritesMenuItem(isFavorite: isFavorite, bookmark: bookmark), + NSMenuItem.separator(), + editBookmarkMenuItem(bookmark: bookmark), + copyBookmarkMenuItem(bookmark: bookmark), + deleteBookmarkMenuItem(bookmark: bookmark), + moveToEndMenuItem(entity: bookmark, parent: parent), + NSMenuItem.separator(), + addFolderMenuItem(folder: nil), + manageBookmarksMenuItem(), + ] + } - menu.addItem(openInNewTabsMenuItem(folder: folder)) + static func menuItems(for folder: BookmarkFolder?, parent: BookmarkFolder?) -> [NSMenuItem] { + [ + openInNewTabsMenuItem(folder: folder), + openAllInNewWindowMenuItem(folder: folder), + NSMenuItem.separator(), + editFolderMenuItem(folder: folder, parent: parent), + deleteFolderMenuItem(folder: folder), + moveToEndMenuItem(entity: folder, parent: parent), + NSMenuItem.separator(), + addFolderMenuItem(folder: folder), + manageBookmarksMenuItem(), + ] + } - return menu + static func menuItem(_ title: String, _ action: Selector, _ representedObject: Any? = nil) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: "") + item.representedObject = representedObject + return item } - // MARK: - Menu Items + // MARK: - Single Bookmark Menu Items - static func newFolderMenuItem() -> NSMenuItem { - return menuItem(UserText.newFolder, #selector(FolderMenuItemSelectors.newFolder(_:))) + static func openBookmarkInNewTabMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.openInNewTab, #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), bookmark) } - static func renameFolderMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.renameFolder, #selector(FolderMenuItemSelectors.renameFolder(_:)), folder) + static func openBookmarkInNewWindowMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.openInNewWindow, #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), bookmark) } - static func editFolderMenuItem(folder: BookmarkFolder, parent: BookmarkFolder?) -> NSMenuItem { - menuItem(UserText.editBookmark, #selector(FolderMenuItemSelectors.editFolder(_:)), (folder, parent)) + static func manageBookmarksMenuItem() -> NSMenuItem { + menuItem(UserText.bookmarksManageBookmarks, #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) } - static func deleteFolderMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.deleteFolder, #selector(FolderMenuItemSelectors.deleteFolder(_:)), folder) + static func addBookmarkToFavoritesMenuItem(isFavorite: Bool, bookmark: Bookmark?) -> NSMenuItem { + let title = isFavorite ? UserText.removeFromFavorites : UserText.addToFavorites + return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmark) } - static func openInNewTabsMenuItem(folder: BookmarkFolder) -> NSMenuItem { - return menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), folder) + static func addBookmarksToFavoritesMenuItem(bookmarks: [Bookmark], allFavorites: Bool) -> NSMenuItem { + let title = allFavorites ? UserText.removeFromFavorites : UserText.addToFavorites + return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmarks) } - static func openBookmarksInNewTabsMenuItem(bookmarks: [Bookmark]) -> NSMenuItem { - return menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), bookmarks) + static func editBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.editBookmark, #selector(BookmarkMenuItemSelectors.editBookmark(_:)), bookmark) } - static func openBookmarkInNewTabMenuItem(bookmark: Bookmark) -> NSMenuItem { - return menuItem(UserText.openInNewTab, #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), bookmark) + static func copyBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.copy, #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), bookmark) } - static func openBookmarkInNewWindowMenuItem(bookmark: Bookmark) -> NSMenuItem { - return menuItem(UserText.openInNewWindow, #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), bookmark) + static func deleteBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { + menuItem(UserText.bookmarksBarContextMenuDelete, #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), bookmark) } - static func addBookmarkToFavoritesMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title: String - - if bookmark.isFavorite { - title = UserText.removeFromFavorites - } else { - title = UserText.addToFavorites - } - - return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmark) + static func moveToEndMenuItem(entity: BaseBookmarkEntity?, parent: BookmarkFolder?) -> NSMenuItem { + let bookmarkEntityInfo = entity.flatMap { BookmarkEntityInfo(entity: $0, parent: parent) } + return menuItem(UserText.bookmarksBarContextMenuMoveToEnd, #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), bookmarkEntityInfo) } - static func addBookmarksToFavoritesMenuItem(bookmarks: [Bookmark], allFavorites: Bool) -> NSMenuItem { - let title: String + // MARK: - Bookmark Folder Menu Items - if allFavorites { - title = UserText.removeFromFavorites - } else { - title = UserText.addToFavorites - } - - return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmarks) + static func openInNewTabsMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.openAllInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), folder) } - static func editBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Edit…", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.editBookmark(_:)), bookmark) + static func openAllInNewWindowMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.openAllTabsInNewWindow, #selector(FolderMenuItemSelectors.openAllInNewWindow(_:)), folder) } - static func copyBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Copy", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), bookmark) + static func addFolderMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.addFolder, #selector(FolderMenuItemSelectors.newFolder(_:)), folder) } - static func deleteBookmarkMenuItem(bookmark: Bookmark) -> NSMenuItem { - let title = NSLocalizedString("Delete", comment: "Command") - return menuItem(title, #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), bookmark) + static func editFolderMenuItem(folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenuItem { + let folderEntityInfo = folder.flatMap { BookmarkEntityInfo(entity: $0, parent: parent) } + return menuItem(UserText.editBookmark, #selector(FolderMenuItemSelectors.editFolder(_:)), folderEntityInfo) } - static func menuItem(_ title: String, _ action: Selector, _ representedObject: Any? = nil) -> NSMenuItem { - let item = NSMenuItem(title: title, action: action, keyEquivalent: "") - item.representedObject = representedObject - return item + static func deleteFolderMenuItem(folder: BookmarkFolder?) -> NSMenuItem { + menuItem(UserText.bookmarksBarContextMenuDelete, #selector(FolderMenuItemSelectors.deleteFolder(_:)), folder) } // MARK: - Multi-Item Menu Creation - private static func menu(for entities: [BaseBookmarkEntity]) -> NSMenu { + static func openBookmarksInNewTabsMenuItem(bookmarks: [Bookmark]) -> NSMenuItem { + menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), bookmarks) + } + + static func menu(for entities: [BaseBookmarkEntity]) -> NSMenu { let menu = NSMenu(title: "") var menuItems: [NSMenuItem] = [] @@ -225,8 +237,7 @@ struct ContextualMenu { menuItems.append(NSMenuItem.separator()) } - let title = NSLocalizedString("Delete", comment: "Command") - let deleteItem = NSMenuItem(title: title, action: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), keyEquivalent: "") + let deleteItem = NSMenuItem(title: UserText.bookmarksBarContextMenuDelete, action: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), keyEquivalent: "") deleteItem.representedObject = entities menuItems.append(deleteItem) diff --git a/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift b/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift index b6cf86e6b1..393b22de8e 100644 --- a/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift +++ b/DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift @@ -18,7 +18,13 @@ import AppKit -@objc protocol BookmarkMenuItemSelectors { +@objc protocol BookmarksMenuItemSelectors { + func newFolder(_ sender: NSMenuItem) + func moveToEnd(_ sender: NSMenuItem) + @objc optional func manageBookmarks(_ sender: NSMenuItem) +} + +@objc protocol BookmarkMenuItemSelectors: BookmarksMenuItemSelectors { func openBookmarkInNewTab(_ sender: NSMenuItem) func openBookmarkInNewWindow(_ sender: NSMenuItem) @@ -30,12 +36,11 @@ import AppKit } -@objc protocol FolderMenuItemSelectors { +@objc protocol FolderMenuItemSelectors: BookmarksMenuItemSelectors { - func newFolder(_ sender: NSMenuItem) - func renameFolder(_ sender: NSMenuItem) func editFolder(_ sender: NSMenuItem) func deleteFolder(_ sender: NSMenuItem) func openInNewTabs(_ sender: NSMenuItem) + func openAllInNewWindow(_ sender: NSMenuItem) } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index b598421312..4830f8d2c2 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -347,8 +347,7 @@ final class BookmarkListViewController: NSViewController { } @objc func openManagementInterface(_ sender: NSButton) { - WindowControllersManager.shared.showBookmarksTab() - delegate?.popoverShouldClose(self) + showManageBookmarks() } @objc func handleClick(_ sender: NSOutlineView) { @@ -447,6 +446,11 @@ private extension BookmarkListViewController { } } + func showManageBookmarks() { + WindowControllersManager.shared.showBookmarksTab() + delegate?.popoverShouldClose(self) + } + } // MARK: - Menu Item Selectors @@ -555,35 +559,37 @@ extension BookmarkListViewController: BookmarkMenuItemSelectors { bookmarkManager.remove(objectsWithUUIDs: uuids) } -} - -extension BookmarkListViewController: FolderMenuItemSelectors { - - func newFolder(_ sender: NSMenuItem) { - newFolderButtonClicked(sender) + func manageBookmarks(_ sender: NSMenuItem) { + showManageBookmarks() } - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { - assertionFailure("Failed to retrieve Bookmark from Rename Folder context menu item") + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") return } - delegate?.popover(shouldPreventClosure: true) + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) - .show(in: parent?.view.window) { [weak delegate] in - delegate?.popover(shouldPreventClosure: false) - } +} + +extension BookmarkListViewController: FolderMenuItemSelectors { + + func newFolder(_ sender: NSMenuItem) { + newFolderButtonClicked(sender) } func editFolder(_ sender: NSMenuItem) { - guard let (folder, parent) = sender.representedObject as? (BookmarkFolder, BookmarkFolder?) else { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { assertionFailure("Failed to retrieve Bookmark from Edit Folder context menu item") return } - let view = BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: parent) + let view = BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) showDialog(view: view) } @@ -598,15 +604,28 @@ extension BookmarkListViewController: FolderMenuItemSelectors { func openInNewTabs(_ sender: NSMenuItem) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, - let children = (sender.representedObject as? BookmarkFolder)?.children else { - assertionFailure("Cannot open in new tabs") + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new tabs") return } - let tabs = children.compactMap { ($0 as? Bookmark)?.urlObject }.map { Tab(content: .url($0, source: .bookmark), shouldLoadInBackground: true, burnerMode: tabCollection.burnerMode) } + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) tabCollection.append(tabs: tabs) } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } // MARK: - BookmarkListPopover diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index cf16261672..ecc643b33d 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -602,7 +602,8 @@ extension BookmarkManagementDetailViewController: NSMenuDelegate { return ContextualMenu.menu(for: nil) } - if tableView.selectedRowIndexes.contains(row) { + // If only one item is selected try to get the item and its parent folder otherwise show the menu for multiple items. + if tableView.selectedRowIndexes.contains(row), tableView.selectedRowIndexes.count > 1 { return ContextualMenu.menu(for: self.selectedItems()) } @@ -639,23 +640,15 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { presentAddFolderModal(sender) } - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { - assertionFailure("Failed to cast menu represented object to BookmarkFolder") - return - } - - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) - .show(in: view.window) - } - func editFolder(_ sender: NSMenuItem) { - guard let (folder, parent) = sender.representedObject as? (BookmarkFolder, BookmarkFolder?) else { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { assertionFailure("Failed to cast menu represented object to BookmarkFolder") return } - BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: parent) + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) .show(in: view.window) } @@ -668,6 +661,16 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { bookmarkManager.remove(folder: folder) } + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") + return + } + + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } + func openInNewTabs(_ sender: NSMenuItem) { if let children = (sender.representedObject as? BookmarkFolder)?.children { let bookmarks = children.compactMap { $0 as? Bookmark } @@ -679,6 +682,18 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { } } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } extension BookmarkManagementDetailViewController: BookmarkMenuItemSelectors { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index 2c9b846841..a413548f0e 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -292,26 +292,20 @@ extension BookmarkManagementSidebarViewController: NSMenuDelegate { extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { func newFolder(_ sender: NSMenuItem) { - AddBookmarkFolderModalView().show(in: view.window) - } - - func renameFolder(_ sender: NSMenuItem) { - guard let folder = sender.representedObject as? BookmarkFolder else { - assertionFailure("Failed to retrieve Bookmark from Rename Folder context menu item") - return - } - - AddBookmarkFolderModalView(model: AddBookmarkFolderModalViewModel(folder: folder)) + let parent = sender.representedObject as? BookmarkFolder + BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parent) .show(in: view.window) } func editFolder(_ sender: NSMenuItem) { - guard let (folder, parent) = sender.representedObject as? (BookmarkFolder, BookmarkFolder?) else { + guard let bookmarkEntityInfo = sender.representedObject as? BookmarkEntityInfo, + let folder = bookmarkEntityInfo.entity as? BookmarkFolder + else { assertionFailure("Failed to cast menu represented object to BookmarkFolder") return } - BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: parent) + BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: bookmarkEntityInfo.parent) .show(in: view.window) } @@ -324,17 +318,40 @@ extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { bookmarkManager.remove(folder: folder) } + func moveToEnd(_ sender: NSMenuItem) { + guard let bookmarkEntity = sender.representedObject as? BookmarksEntityIdentifiable else { + assertionFailure("Failed to cast menu item's represented object to BookmarkEntity") + return + } + + let parentFolderType: ParentFolderType = bookmarkEntity.parentId.flatMap { .parent(uuid: $0) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmarkEntity.entityId], toIndex: nil, withinParentFolder: parentFolderType) { _ in } + } + func openInNewTabs(_ sender: NSMenuItem) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, - let children = (sender.representedObject as? BookmarkFolder)?.children else { - assertionFailure("Cannot open in new tabs") + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new tabs") return } - let tabs = children.compactMap { ($0 as? Bookmark)?.urlObject }.map { Tab(content: .url($0, source: .bookmark), shouldLoadInBackground: true, burnerMode: tabCollection.burnerMode) } + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) tabCollection.append(tabs: tabs) } + func openAllInNewWindow(_ sender: NSMenuItem) { + guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, + let folder = sender.representedObject as? BookmarkFolder + else { + assertionFailure("Cannot open all in new window") + return + } + + let newTabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollection.burnerMode) + WindowsManager.openNewWindow(with: newTabCollection, isBurner: tabCollection.isBurner) + } + } #if DEBUG diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift index 61c0ec7630..dd9d2e617f 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarCollectionViewItem.swift @@ -24,11 +24,13 @@ protocol BookmarksBarCollectionViewItemDelegate: AnyObject { func bookmarksBarCollectionViewItemOpenInNewTabAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemOpenInNewWindowAction(_ item: BookmarksBarCollectionViewItem) - func bookmarksBarCollectionViewItemAddToFavoritesAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemToggleFavoritesAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewEditAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemMoveToEndAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemCopyBookmarkURLAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemDeleteEntityAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemAddEntityAction(_ item: BookmarksBarCollectionViewItem) + func bookmarksBarCollectionViewItemManageBookmarksAction(_ item: BookmarksBarCollectionViewItem) } @@ -128,114 +130,72 @@ extension BookmarksBarCollectionViewItem: NSMenuDelegate { switch entityType { case .bookmark(_, _, _, let isFavorite): - menu.items = createBookmarkMenuItems(isFavorite: isFavorite) + menu.items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) case .folder: - menu.items = createFolderMenuItems() + menu.items = ContextualMenu.folderMenuItems() } } } -extension BookmarksBarCollectionViewItem { +extension BookmarksBarCollectionViewItem: BookmarkMenuItemSelectors { - // MARK: Bookmark Menu Items - - func createBookmarkMenuItems(isFavorite: Bool) -> [NSMenuItem] { - let items = [ - openBookmarkInNewTabMenuItem(), - openBookmarkInNewWindowMenuItem(), - NSMenuItem.separator(), - addToFavoritesMenuItem(isFavorite: isFavorite), - editItem(), - moveToEndMenuItem(), - NSMenuItem.separator(), - copyBookmarkURLMenuItem(), - deleteEntityMenuItem() - ].compactMap { $0 } - - return items - } - - func openBookmarkInNewTabMenuItem() -> NSMenuItem { - return menuItem(UserText.openInNewTab, #selector(openBookmarkInNewTabMenuItemSelected(_:))) - } - - @objc - func openBookmarkInNewTabMenuItemSelected(_ sender: NSMenuItem) { + func openBookmarkInNewTab(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemOpenInNewTabAction(self) } - func openBookmarkInNewWindowMenuItem() -> NSMenuItem { - return menuItem(UserText.openInNewWindow, #selector(openBookmarkInNewWindowMenuItemSelected(_:))) - } - - @objc - func openBookmarkInNewWindowMenuItemSelected(_ sender: NSMenuItem) { + func openBookmarkInNewWindow(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemOpenInNewWindowAction(self) } - func addToFavoritesMenuItem(isFavorite: Bool) -> NSMenuItem? { - guard !isFavorite else { - return nil - } - - return menuItem(UserText.addToFavorites, #selector(addToFavoritesMenuItemSelected(_:))) - } - - @objc - func addToFavoritesMenuItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewItemAddToFavoritesAction(self) + func toggleBookmarkAsFavorite(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemToggleFavoritesAction(self) } - func editItem() -> NSMenuItem { - return menuItem("Edit…", #selector(editItemSelected(_:))) + func editBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewEditAction(self) } - @objc - func editItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewEditAction(self) + func copyBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemCopyBookmarkURLAction(self) } - func moveToEndMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuMoveToEnd, #selector(moveToEndMenuItemSelected(_:))) + func deleteBookmark(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemDeleteEntityAction(self) } - @objc - func moveToEndMenuItemSelected(_ sender: NSMenuItem) { + func moveToEnd(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemMoveToEndAction(self) } - func copyBookmarkURLMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuCopy, #selector(copyBookmarkURLMenuItemSelected(_:))) + func deleteEntities(_ sender: NSMenuItem) {} + + func manageBookmarks(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemManageBookmarksAction(self) } - @objc - func copyBookmarkURLMenuItemSelected(_ sender: NSMenuItem) { - delegate?.bookmarksBarCollectionViewItemCopyBookmarkURLAction(self) +} + +extension BookmarksBarCollectionViewItem: FolderMenuItemSelectors { + + func newFolder(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemAddEntityAction(self) } - func deleteEntityMenuItem() -> NSMenuItem { - return menuItem(UserText.bookmarksBarContextMenuDelete, #selector(deleteMenuItemSelected(_:))) + func editFolder(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewEditAction(self) } - @objc - func deleteMenuItemSelected(_ sender: NSMenuItem) { + func deleteFolder(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemDeleteEntityAction(self) } - // MARK: Folder Menu Items - - func createFolderMenuItems() -> [NSMenuItem] { - return [ - editItem(), - moveToEndMenuItem(), - NSMenuItem.separator(), - deleteEntityMenuItem() - ] + func openInNewTabs(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemOpenInNewTabAction(self) } - func menuItem(_ title: String, _ action: Selector) -> NSMenuItem { - return NSMenuItem(title: title, action: action, keyEquivalent: "") + func openAllInNewWindow(_ sender: NSMenuItem) { + delegate?.bookmarksBarCollectionViewItemOpenInNewWindowAction(self) } } diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift index 7782688531..b91c8df14b 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift @@ -223,59 +223,105 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { bookmarksBarCollectionView.reloadData() } - private func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for bookmark: Bookmark) { +} + +// MARK: - Private + +private extension BookmarksBarViewController { + + func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for bookmark: Bookmark) { switch action { case .openInNewTab: - guard let url = bookmark.urlObject else { return } - tabCollectionViewModel.appendNewTab(with: .url(url, source: .bookmark), selected: true) + openInNewTab(bookmark: bookmark) case .openInNewWindow: - guard let url = bookmark.urlObject else { return } - WindowsManager.openNewWindow(with: url, source: .bookmark, isBurner: false) + openInNewWindow(bookmark: bookmark) case .clickItem: WindowControllersManager.shared.open(bookmark: bookmark) - case .addToFavorites: - bookmark.isFavorite = true + case .toggleFavorites: + bookmark.isFavorite.toggle() bookmarkManager.update(bookmark: bookmark) case .edit: - BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark) - .show(in: view.window) + showDialog(view: BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark)) case .moveToEnd: bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: .root) { _ in } case .copyURL: bookmark.copyUrlToPasteboard() case .deleteEntity: bookmarkManager.remove(bookmark: bookmark) + case .addFolder: + showDialog(view: BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil)) + case .manageBookmarks: + manageBookmarks() } } - private func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for folder: BookmarkFolder, item: BookmarksBarCollectionViewItem) { + func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for folder: BookmarkFolder, item: BookmarksBarCollectionViewItem) { switch action { case .clickItem: - let childEntities = folder.children - let viewModels = childEntities.map { BookmarkViewModel(entity: $0) } - let menuItems = viewModel.bookmarksTreeMenuItems(from: viewModels, topLevel: true) - let menu = bookmarkFolderMenu(items: menuItems) - - menu.popUp(positioning: nil, at: CGPoint(x: 0, y: item.view.frame.minY - 7), in: item.view) + showSubmenuFor(folder: folder, fromView: item.view) case .edit: - BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: nil) - .show(in: view.window) + showDialog(view: BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: folder, parentFolder: nil)) case .moveToEnd: bookmarkManager.move(objectUUIDs: [folder.id], toIndex: nil, withinParentFolder: .root) { _ in } case .deleteEntity: bookmarkManager.remove(folder: folder) + case .addFolder: + showDialog(view: BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: folder)) + case .openInNewTab: + openAllInNewTabs(folder: folder) + case .openInNewWindow: + openAllInNewWindow(folder: folder) + case .manageBookmarks: + manageBookmarks() default: assertionFailure("Received unexpected action for bookmark folder") } } - private func bookmarkFolderMenu(items: [NSMenuItem]) -> NSMenu { + func bookmarkFolderMenu(items: [NSMenuItem]) -> NSMenu { let menu = NSMenu() menu.items = items.isEmpty ? [NSMenuItem.empty] : items menu.autoenablesItems = false return menu } + func openInNewTab(bookmark: Bookmark) { + guard let url = bookmark.urlObject else { return } + tabCollectionViewModel.appendNewTab(with: .url(url, source: .bookmark), selected: true) + } + + func openInNewWindow(bookmark: Bookmark) { + guard let url = bookmark.urlObject else { return } + WindowsManager.openNewWindow(with: url, source: .bookmark, isBurner: false) + } + + func openAllInNewTabs(folder: BookmarkFolder) { + let tabs = Tab.withContentOfBookmark(folder: folder, burnerMode: tabCollectionViewModel.burnerMode) + tabCollectionViewModel.append(tabs: tabs) + } + + func openAllInNewWindow(folder: BookmarkFolder) { + let tabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: tabCollectionViewModel.burnerMode) + WindowsManager.openNewWindow(with: tabCollection, isBurner: tabCollectionViewModel.isBurner) + } + + func showSubmenuFor(folder: BookmarkFolder, fromView view: NSView) { + let childEntities = folder.children + let viewModels = childEntities.map { BookmarkViewModel(entity: $0) } + let menuItems = viewModel.bookmarksTreeMenuItems(from: viewModels, topLevel: true) + let menu = bookmarkFolderMenu(items: menuItems) + + menu.popUp(positioning: nil, at: CGPoint(x: 0, y: view.frame.minY - 7), in: view) + } + + func showDialog(view: any ModalView) { + view.show(in: self.view.window) + } + + func manageBookmarks() { + WindowControllersManager.shared.showBookmarksTab() + } + } // MARK: - Menu diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift index 2dc81a1596..1c82b4c576 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift @@ -47,11 +47,13 @@ final class BookmarksBarViewModel: NSObject { case clickItem case openInNewTab case openInNewWindow - case addToFavorites + case toggleFavorites case edit case moveToEnd case copyURL case deleteEntity + case addFolder + case manageBookmarks } struct BookmarksBarItem { @@ -482,8 +484,8 @@ extension BookmarksBarViewModel: BookmarksBarCollectionViewItemDelegate { delegate?.bookmarksBarViewModelReceived(action: .openInNewWindow, for: item) } - func bookmarksBarCollectionViewItemAddToFavoritesAction(_ item: BookmarksBarCollectionViewItem) { - delegate?.bookmarksBarViewModelReceived(action: .addToFavorites, for: item) + func bookmarksBarCollectionViewItemToggleFavoritesAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .toggleFavorites, for: item) } func bookmarksBarCollectionViewEditAction(_ item: BookmarksBarCollectionViewItem) { @@ -502,4 +504,12 @@ extension BookmarksBarViewModel: BookmarksBarCollectionViewItemDelegate { delegate?.bookmarksBarViewModelReceived(action: .deleteEntity, for: item) } + func bookmarksBarCollectionViewItemAddEntityAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .addFolder, for: item) + } + + func bookmarksBarCollectionViewItemManageBookmarksAction(_ item: BookmarksBarCollectionViewItem) { + delegate?.bookmarksBarViewModelReceived(action: .manageBookmarks, for: item) + } + } diff --git a/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift b/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift new file mode 100644 index 0000000000..5226ead0c6 --- /dev/null +++ b/UnitTests/Bookmarks/Extensions/Bookmarks+TabTests.swift @@ -0,0 +1,61 @@ +// +// Bookmarks+TabTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class Bookmarks_TabTests: XCTestCase { + + func testWhenBuildTabsWithContentOfFolderThenItShouldReturnAsManyTabsAsBookmarksWithinTheFolder() { + // GIVEN + let bookmark1 = Bookmark(id: "A", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let bookmark2 = Bookmark(id: "B", url: URL.atb, title: "ATB", isFavorite: false) + let subFolder = BookmarkFolder(id: "C", title: "Child") + let folder = BookmarkFolder(id: "1", title: "Test", children: [bookmark1, subFolder, bookmark2]) + + // WHEN + let tab = Tab.withContentOfBookmark(folder: folder, burnerMode: .regular) + + // THEN + XCTAssertEqual(tab.count, 2) + XCTAssertEqual(tab.first?.url?.absoluteString, bookmark1.url) + XCTAssertEqual(tab.first?.burnerMode, .regular) + XCTAssertEqual(tab.last?.url?.absoluteString, bookmark2.url) + XCTAssertEqual(tab.last?.burnerMode, .regular) + } + + func testWhenBuildTabCollectionWithContentOfFolderThenItShouldReturnACollectionWithAsManyTabsAsBookmarksWithinTheFolder() { + // GIVEN + let bookmark1 = Bookmark(id: "A", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let bookmark2 = Bookmark(id: "B", url: URL.atb, title: "ATB", isFavorite: false) + let subFolder = BookmarkFolder(id: "C", title: "Child") + let folder = BookmarkFolder(id: "1", title: "Test", children: [bookmark1, subFolder, bookmark2]) + + // WHEN + let tabCollection = TabCollection.withContentOfBookmark(folder: folder, burnerMode: .regular) + + // THEN + XCTAssertEqual(tabCollection.tabs.count, 2) + XCTAssertEqual(tabCollection.tabs.first?.url?.absoluteString, bookmark1.url) + XCTAssertEqual(tabCollection.tabs.first?.burnerMode, .regular) + XCTAssertEqual(tabCollection.tabs.last?.url?.absoluteString, bookmark2.url) + XCTAssertEqual(tabCollection.tabs.last?.burnerMode, .regular) + } + +} diff --git a/UnitTests/Bookmarks/Model/ContextualMenuTests.swift b/UnitTests/Bookmarks/Model/ContextualMenuTests.swift new file mode 100644 index 0000000000..d2122ff244 --- /dev/null +++ b/UnitTests/Bookmarks/Model/ContextualMenuTests.swift @@ -0,0 +1,267 @@ +// +// ContextualMenuTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class ContextualMenuTests: XCTestCase { + + func testWhenAskingBookmarkMenuItemsAndIsNotFavoriteThenItShouldReturnTheItemsInTheCorrectOrder() { + // GIVEN + let isFavorite = false + + // WHEN + let items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) + + // THEN + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:))) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:))) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:))) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:))) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + + } + + func testWhenAskingBookmarkMenuItemsAndIsFavoriteThenItShouldReturnTheItemsInTheCorrectOrder() { + // GIVEN + let isFavorite = true + + // WHEN + let items = ContextualMenu.bookmarkMenuItems(isFavorite: isFavorite) + + // THEN + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:))) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.removeFromFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:))) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:))) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:))) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenAskingFolderItemThenItShouldReturnTheItemsInTheCorrectOrders() { + // WHEN + let items = ContextualMenu.folderMenuItems() + + // THEN + XCTAssertEqual(items.count, 9) + assertMenu(item: items[0], withTitle: UserText.openAllInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:))) + assertMenu(item: items[1], withTitle: UserText.openAllTabsInNewWindow, selector: #selector(FolderMenuItemSelectors.openAllInNewWindow(_:))) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.editBookmark, selector: #selector(FolderMenuItemSelectors.editFolder(_:))) + assertMenu(item: items[4], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(FolderMenuItemSelectors.deleteFolder(_:))) + assertMenu(item: items[5], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(FolderMenuItemSelectors.moveToEnd(_:))) + assertMenu(item: items[6], withTitle: "", selector: nil) // Separator + assertMenu(item: items[7], withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[8], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(FolderMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForEmptySelectionThenItReturnsAMenuWithAddFolderOnly() throws { + // WHEN + let menu = ContextualMenu.menu(for: []) + + // THEN + XCTAssertEqual(menu?.items.count, 1) + let menuItem = try XCTUnwrap(menu?.items.first) + assertMenu(item: menuItem, withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:))) + } + + func testWhenCreateMenuForBookmarkWithoutParentThenReturnsAMenuWithTheBookmarkMenuItems() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), representedObject: bookmark) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), representedObject: bookmark) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: bookmark) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: bookmark, parent: nil)) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForBookmarkWithParentThenReturnsAMenuWithTheBookmarkMenuItems() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "A") + let parent = BookmarkFolder(id: "A", title: "Folder", children: [bookmark]) + let parentNode = BookmarkNode(representedObject: parent, parent: nil) + let node = BookmarkNode(representedObject: bookmark, parent: parentNode) + + // WHEN + let menu = ContextualMenu.menu(for: [node]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 12) + assertMenu(item: items[0], withTitle: UserText.openInNewTab, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewTab(_:)), representedObject: bookmark) + assertMenu(item: items[1], withTitle: UserText.openInNewWindow, selector: #selector(BookmarkMenuItemSelectors.openBookmarkInNewWindow(_:)), representedObject: bookmark) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: bookmark) + assertMenu(item: items[4], withTitle: "", selector: nil) // Separator + assertMenu(item: items[5], withTitle: UserText.editBookmark, selector: #selector(BookmarkMenuItemSelectors.editBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[6], withTitle: UserText.bookmarksBarContextMenuCopy, selector: #selector(BookmarkMenuItemSelectors.copyBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), representedObject: bookmark) + assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: bookmark, parent: parent)) + assertMenu(item: items[9], withTitle: "", selector: nil) // Separator + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForFolderNodeThenReturnsAMenuWithTheFolderMenuItems() throws { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Child") + let parent = BookmarkFolder(id: "1", title: "Parent", children: [folder]) + let parentNode = BookmarkNode(representedObject: parent, parent: nil) + let node = BookmarkNode(representedObject: folder, parent: parentNode) + + // WHEN + let menu = ContextualMenu.menu(for: [node]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 9) + assertMenu(item: items[0], withTitle: UserText.openAllInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: folder) + assertMenu(item: items[1], withTitle: UserText.openAllTabsInNewWindow, selector: #selector(FolderMenuItemSelectors.openAllInNewWindow(_:)), representedObject: folder) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.editBookmark, selector: #selector(FolderMenuItemSelectors.editFolder(_:)), representedObject: BookmarkEntityInfo(entity: folder, parent: parent)) + assertMenu(item: items[4], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(FolderMenuItemSelectors.deleteFolder(_:)), representedObject: folder) + assertMenu(item: items[5], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(FolderMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: folder, parent: parent)) + assertMenu(item: items[6], withTitle: "", selector: nil) // Separator + assertMenu(item: items[7], withTitle: UserText.addFolder, selector: #selector(FolderMenuItemSelectors.newFolder(_:)), representedObject: folder) + assertMenu(item: items[8], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(FolderMenuItemSelectors.manageBookmarks(_:))) + } + + func testWhenCreateMenuForMultipleUnfavoriteBookmarksThenReturnsMenuWithOpenInNewTabsAddToFavoritesAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: false) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 4) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: UserText.addToFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForMultipleFavoriteBookmarksThenReturnsMenuWithOpenInNewTabsRemoveFromFavoritesAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: true) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: true) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 4) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: UserText.removeFromFavorites, selector: #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[2], withTitle: "", selector: nil) // Separator + assertMenu(item: items[3], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForMultipleMixedFavoriteBookmarksThenReturnsMenuWithOpenInNewTabsAndDelete() throws { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: "", title: "", isFavorite: true) + let bookmark2 = Bookmark(id: "2", url: "", title: "", isFavorite: false) + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark1, bookmark2]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 3) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark1, bookmark2]) + assertMenu(item: items[1], withTitle: "", selector: nil) // Separator + assertMenu(item: items[2], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark1, bookmark2]) + } + + func testWhenCreateMenuForBookmarkAndFolderThenReturnsMenuWithOpenInNewTabsOnlyForBookmarkAndDelete() throws { + // GIVEN + let bookmark = Bookmark(id: "1", url: "", title: "Bookmark", isFavorite: true) + let folder = BookmarkFolder(id: "1", title: "Folder") + + // WHEN + let menu = ContextualMenu.menu(for: [bookmark, folder]) + + // THEN + let items = try XCTUnwrap(menu?.items) + XCTAssertEqual(items.count, 3) + assertMenu(item: items[0], withTitle: UserText.bookmarksOpenInNewTabs, selector: #selector(FolderMenuItemSelectors.openInNewTabs(_:)), representedObject: [bookmark]) + assertMenu(item: items[1], withTitle: "", selector: nil) // Separator + assertMenu(item: items[2], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark, folder]) + } + +} + +private extension ContextualMenuTests { + + func assertMenu(item: NSMenuItem, withTitle title: String, selector: Selector?, representedObject: T = Empty() ) { + XCTAssertEqual(item.title, title) + XCTAssertEqual(item.action, selector) + if representedObject is Empty { + XCTAssertNil(item.representedObject) + } else { + XCTAssertEqualValue(item.representedObject, representedObject) + } + } + +} + +private struct Empty: Equatable {} + +private func XCTAssertEqualValue(_ expression1: @autoclosure () throws -> Any?, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) where T: Equatable { + do { + guard let firstValue = try expression1() as? T else { + XCTFail("Type of expression1 \(type(of: try? expression1())) and expression2 \(type(of: try? expression2())) are different.") + return + } + let secondValue = try expression2() + XCTAssertEqual(firstValue, secondValue, message(), file: file, line: line) + } catch { + XCTFail("Failed evaluating expression.") + } +} diff --git a/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift b/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift index 32d827186b..269980c361 100644 --- a/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift +++ b/UnitTests/BookmarksBar/ViewModel/BookmarksBarViewModelTests.swift @@ -91,6 +91,199 @@ class BookmarksBarViewModelTests: XCTestCase { XCTAssert(bookmarksBarViewModel.clippedItems.isEmpty) } + // MARK: - Bookmarks Delegate + + func testWhenItemFiresClickedActionThenDelegateReceivesClickItemActionAndPreventClickIsFalse() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemClicked(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .clickItem) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + + } + + func testWhenItemFiresOpenInNewTabActionThenDelegateReceivesOpenInNewTabAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemOpenInNewTabAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .openInNewTab) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresOpenInNewWindowActionThenDelegateReceivesOpenInNewWindowAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemOpenInNewWindowAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .openInNewWindow) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresToggleFavoritesActionThenDelegateReceivesToggleFavoritesAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemToggleFavoritesAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .toggleFavorites) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresEditActionThenDelegateReceivesEditAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewEditAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .edit) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresMoveToEndActionThenDelegateReceivesMoveToEndAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemMoveToEndAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .moveToEnd) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresCopyBookmarkURLActionThenDelegateReceivesCopyBookmarkURLAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemCopyBookmarkURLAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .copyURL) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresDeleteEntityActionThenDelegateReceivesDeleteEntityAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemDeleteEntityAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .deleteEntity) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresAddEntityActionThenDelegateReceivesAddEntityAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemAddEntityAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .addFolder) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + + func testWhenItemFiresManageBookmarksActionThenDelegateReceivesManageBookmarksAction() { + // GIVEN + let sut = BookmarksBarViewModel(bookmarkManager: createMockBookmarksManager(), tabCollectionViewModel: .mock()) + let collectionViewItem = BookmarksBarCollectionViewItem() + let delegateMock = BookmarksBarViewModelDelegateMock() + sut.delegate = delegateMock + XCTAssertFalse(delegateMock.didCallViewModelReceivedAction) + XCTAssertNil(delegateMock.capturedAction) + XCTAssertNil(delegateMock.capturedItem) + + // WHEN + sut.bookmarksBarCollectionViewItemManageBookmarksAction(collectionViewItem) + + // THEN + XCTAssertTrue(delegateMock.didCallViewModelReceivedAction) + XCTAssertEqual(delegateMock.capturedAction, .manageBookmarks) + XCTAssertEqual(delegateMock.capturedItem, collectionViewItem) + } + private func createMockBookmarksManager(mockBookmarkStore: BookmarkStoreMock = BookmarkStoreMock()) -> BookmarkManager { let mockFaviconManager = FaviconManagerMock() return LocalBookmarkManager(bookmarkStore: mockBookmarkStore, faviconManagement: mockFaviconManager) @@ -107,3 +300,24 @@ fileprivate extension TabCollectionViewModel { } } + +// MARK: - BookmarksBarViewModelDelegateMock + +final class BookmarksBarViewModelDelegateMock: BookmarksBarViewModelDelegate { + private(set) var didCallViewModelReceivedAction = false + private(set) var capturedAction: BookmarksBarViewModel.BookmarksBarItemAction? + private(set) var capturedItem: BookmarksBarCollectionViewItem? + + func bookmarksBarViewModelReceived(action: BookmarksBarViewModel.BookmarksBarItemAction, for item: BookmarksBarCollectionViewItem) { + didCallViewModelReceivedAction = true + capturedAction = action + capturedItem = item + } + + func bookmarksBarViewModelWidthForContainer() -> CGFloat { + 0 + } + + func bookmarksBarViewModelReloadedData() {} + +} From a90da951bcab500703326af154020b62eb5badd8 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 7 Mar 2024 15:21:58 +1100 Subject: [PATCH 10/19] Alessandro/bookmarks fix shortcut panel (#2313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206687983236914/f **Description**: Fix an issue where the Bookmark panel accessible via the Bookmarks shortcut wasn’t reloading when updating the Bookmark URL. --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + DuckDuckGo/Bookmarks/Model/Bookmark.swift | 68 +++++- DuckDuckGo/Bookmarks/Model/BookmarkList.swift | 29 ++- .../Bookmarks/Model/BookmarkManager.swift | 18 ++ DuckDuckGo/Bookmarks/Model/BookmarkNode.swift | 21 +- .../Model/BookmarkOutlineViewDataSource.swift | 2 +- .../Bookmarks/Model/PasteboardFolder.swift | 25 +- .../View/BookmarkListViewController.swift | 3 + .../AddEditBookmarkDialogViewModel.swift | 19 +- .../View/BookmarksBarViewModel.swift | 4 + .../Model/BaseBookmarkEntityTests.swift | 223 ++++++++++++++++++ .../Bookmarks/Model/BookmarkListTests.swift | 59 ++++- .../Bookmarks/Model/BookmarkNodeTests.swift | 170 +++++++++++++ .../BookmarkOutlineViewDataSourceTests.swift | 6 +- UnitTests/Bookmarks/Model/BookmarkTests.swift | 2 +- .../Services/LocalBookmarkStoreTests.swift | 48 ++-- .../AddEditBookmarkDialogViewModelTests.swift | 33 ++- .../HomePage/Mocks/MockBookmarkManager.swift | 2 + 18 files changed, 663 insertions(+), 75 deletions(-) create mode 100644 UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8057dec63c..54baf99a04 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2409,6 +2409,8 @@ 9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DB6E7232AA0DC5800A17F3C /* LoginItems */; }; 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC70B192AA1FA5B005A844B /* LoginItems */; }; 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEF97E02B06C4EE00764F03 /* Networking */; }; + 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; + 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */; }; 9F180D0F2B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; @@ -4046,6 +4048,7 @@ 9D9AE9282AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionBackgroundManager.swift; sourceTree = ""; }; 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMocks.swift; sourceTree = ""; }; 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; + 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBookmarkEntityTests.swift; sourceTree = ""; }; 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+WKUIDelegateTests.swift"; sourceTree = ""; }; 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelTests.swift; sourceTree = ""; }; @@ -7134,6 +7137,7 @@ AA652CD225DDA6E9009059CC /* LocalBookmarkManagerTests.swift */, 98A95D87299A2DF900B9B81A /* BookmarkMigrationTests.swift */, 9F872D9F2B90644800138637 /* ContextualMenuTests.swift */, + 9F0A2CF72B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift */, ); path = Model; sourceTree = ""; @@ -10755,6 +10759,7 @@ 3706FE54293F661700E42796 /* PasteboardBookmarkTests.swift in Sources */, 3706FE55293F661700E42796 /* CBRCompileTimeReporterTests.swift in Sources */, 566B196429CDB824007E38F4 /* MoreOptionsMenuTests.swift in Sources */, + 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 3706FE56293F661700E42796 /* FaviconManagerMock.swift in Sources */, 3706FE57293F661700E42796 /* LocalPinningManagerTests.swift in Sources */, 3706FE58293F661700E42796 /* HistoryStoreTests.swift in Sources */, @@ -12711,6 +12716,7 @@ B63ED0DC26AE7B1E00A9DAD1 /* WebViewMock.swift in Sources */, 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */, AAE39D1B24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift in Sources */, + 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 373A1AAA283ED86C00586521 /* BookmarksHTMLReaderTests.swift in Sources */, 317295D42AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */, AA9C363025518CA9004B1BA3 /* FireTests.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Model/Bookmark.swift b/DuckDuckGo/Bookmarks/Model/Bookmark.swift index 99ca656b3f..914fdd28d3 100644 --- a/DuckDuckGo/Bookmarks/Model/Bookmark.swift +++ b/DuckDuckGo/Bookmarks/Model/Bookmark.swift @@ -19,7 +19,7 @@ import Cocoa import Bookmarks -internal class BaseBookmarkEntity: Identifiable { +internal class BaseBookmarkEntity: Identifiable, Equatable, Hashable { static func singleEntity(with uuid: String) -> NSFetchRequest { let request = BookmarkEntity.fetchRequest() @@ -92,6 +92,23 @@ internal class BaseBookmarkEntity: Identifiable { } } + // Subclasses needs to override to check equality on their properties + func isEqual(to instance: BaseBookmarkEntity) -> Bool { + id == instance.id && + title == instance.title && + isFolder == instance.isFolder + } + + static func == (lhs: BaseBookmarkEntity, rhs: BaseBookmarkEntity) -> Bool { + return type(of: lhs) == type(of: rhs) && lhs.isEqual(to: rhs) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + } + } final class BookmarkFolder: BaseBookmarkEntity { @@ -129,6 +146,26 @@ final class BookmarkFolder: BaseBookmarkEntity { super.init(id: id, title: title, isFolder: true) } + + override func isEqual(to instance: BaseBookmarkEntity) -> Bool { + guard let folder = instance as? BookmarkFolder else { + return false + } + return id == folder.id && + title == folder.title && + isFolder == folder.isFolder && + parentFolderUUID == folder.parentFolderUUID && + children == folder.children + } + + override func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + hasher.combine(parentFolderUUID) + hasher.combine(children) + } + } final class Bookmark: BaseBookmarkEntity { @@ -183,12 +220,33 @@ final class Bookmark: BaseBookmarkEntity { parentFolderUUID: bookmark.parentFolderUUID) } -} + convenience init(from bookmark: Bookmark, withNewUrl url: String, title: String, isFavorite: Bool) { + self.init(id: bookmark.id, + url: url, + title: title, + isFavorite: isFavorite, + parentFolderUUID: bookmark.parentFolderUUID) + } -extension BaseBookmarkEntity: Equatable { + override func isEqual(to instance: BaseBookmarkEntity) -> Bool { + guard let bookmark = instance as? Bookmark else { + return false + } + return id == bookmark.id && + title == bookmark.title && + isFolder == bookmark.isFolder && + url == bookmark.url && + isFavorite == bookmark.isFavorite && + parentFolderUUID == bookmark.parentFolderUUID + } - static func == (lhs: BaseBookmarkEntity, rhs: BaseBookmarkEntity) -> Bool { - return lhs.id == rhs.id + override func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(title) + hasher.combine(isFolder) + hasher.combine(url) + hasher.combine(isFavorite) + hasher.combine(parentFolderUUID) } } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift index 2fe7281adb..89e82c2a19 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift @@ -124,6 +124,13 @@ struct BookmarkList { } } + mutating func update(bookmark: Bookmark, newURL: String, newTitle: String, newIsFavorite: Bool) -> Bookmark? { + guard !bookmark.isFolder else { return nil } + + let newBookmark = Bookmark(from: bookmark, withNewUrl: newURL, title: newTitle, isFavorite: newIsFavorite) + return updateBookmarkList(newBookmark: newBookmark, oldBookmark: bookmark) + } + mutating func updateUrl(of bookmark: Bookmark, to newURL: String) -> Bookmark? { guard !bookmark.isFolder else { return nil } @@ -131,12 +138,25 @@ struct BookmarkList { os_log("BookmarkList: Update failed, new url already in bookmark list") return nil } + + let newBookmark = Bookmark(from: bookmark, with: newURL) + return updateBookmarkList(newBookmark: newBookmark, oldBookmark: bookmark) + } + + func bookmarks() -> [IdentifiableBookmark] { + return allBookmarkURLsOrdered + } + +} + +private extension BookmarkList { + + mutating private func updateBookmarkList(newBookmark: Bookmark, oldBookmark bookmark: Bookmark) -> Bookmark? { guard itemsDict[bookmark.url] != nil, let index = allBookmarkURLsOrdered.firstIndex(of: IdentifiableBookmark(from: bookmark)) else { os_log("BookmarkList: Update failed, no such item in bookmark list") return nil } - let newBookmark = Bookmark(from: bookmark, with: newURL) let newIdentifiableBookmark = IdentifiableBookmark(from: newBookmark) allBookmarkURLsOrdered.remove(at: index) @@ -146,13 +166,8 @@ struct BookmarkList { let updatedBookmarks = existingBookmarks.filter { $0.id != bookmark.id } itemsDict[bookmark.url] = updatedBookmarks - itemsDict[newURL] = (itemsDict[newURL] ?? []) + [bookmark] + itemsDict[newBookmark.url] = (itemsDict[newBookmark.url] ?? []) + [newBookmark] return newBookmark } - - func bookmarks() -> [IdentifiableBookmark] { - return allBookmarkURLsOrdered - } - } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index 7e249c3b48..b3172da78e 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -35,6 +35,7 @@ protocol BookmarkManager: AnyObject { func remove(folder: BookmarkFolder) func remove(objectsWithUUIDs uuids: [String]) func update(bookmark: Bookmark) + func update(bookmark: Bookmark, withURL url: URL, title: String, isFavorite: Bool) func update(folder: BookmarkFolder) func update(folder: BookmarkFolder, andMoveToParent parent: ParentFolderType) @discardableResult func updateUrl(of bookmark: Bookmark, to newUrl: URL) -> Bookmark? @@ -212,6 +213,23 @@ final class LocalBookmarkManager: BookmarkManager { } + func update(bookmark: Bookmark, withURL url: URL, title: String, isFavorite: Bool) { + guard list != nil else { return } + guard getBookmark(forUrl: bookmark.url) != nil else { + os_log("LocalBookmarkManager: Failed to update bookmark url - not in the list.", type: .error) + return + } + + guard let newBookmark = list?.update(bookmark: bookmark, newURL: url.absoluteString, newTitle: title, newIsFavorite: isFavorite) else { + os_log("LocalBookmarkManager: Failed to update URL of bookmark.", type: .error) + return + } + + bookmarkStore.update(bookmark: newBookmark) + loadBookmarks() + requestSync() + } + func update(folder: BookmarkFolder) { bookmarkStore.update(folder: folder) loadBookmarks() diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift b/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift index 86f24bdc28..4f6b58c10f 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkNode.swift @@ -67,11 +67,24 @@ final class BookmarkNode: Hashable { return 0 } - init(representedObject: AnyObject, parent: BookmarkNode?) { + /// Creates an instance of a bookmark node. + /// - Parameters: + /// - representedObject: The represented object contained in the node. + /// - parent: An optional parent node. + /// - uniqueId: A unique identifier for the node. This should be used only in unit tests. + /// - Attention: Use this initializer only in tests. + init(representedObject: AnyObject, parent: BookmarkNode?, uniqueId: Int) { self.representedObject = representedObject self.parent = parent - self.uniqueID = BookmarkNode.incrementingID + self.uniqueID = uniqueId + } + /// Creates an instance of a bookmark node. + /// - Parameters: + /// - representedObject: The represented object contained in the node. + /// - parent: An optional parent node. + convenience init(representedObject: AnyObject, parent: BookmarkNode?) { + self.init(representedObject: representedObject, parent: parent, uniqueId: BookmarkNode.incrementingID) BookmarkNode.incrementingID += 1 } @@ -165,7 +178,7 @@ final class BookmarkNode: Hashable { // The Node class will most frequently represent Bookmark entities and PseudoFolders. Because of this, their unique properties are // used to derive the hash for the node so that equality can be handled based on the represented object. if let entity = self.representedObject as? BaseBookmarkEntity { - hasher.combine(entity.id) + hasher.combine(entity.hashValue) } else if let folder = self.representedObject as? PseudoFolder { hasher.combine(folder.name) } else { @@ -176,7 +189,7 @@ final class BookmarkNode: Hashable { // MARK: - Equatable class func == (lhs: BookmarkNode, rhs: BookmarkNode) -> Bool { - return lhs === rhs + return lhs.uniqueID == rhs.uniqueID && lhs.representedObjectEquals(rhs.representedObject) } } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index 9abad4863d..a1479ba73d 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -241,7 +241,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS // Folders cannot be dragged onto any of their descendants: let containsDescendantOfDestination = draggedFolders.contains { draggedFolder in - let folder = BookmarkFolder(id: draggedFolder.id, title: draggedFolder.name) + let folder = BookmarkFolder(id: draggedFolder.id, title: draggedFolder.name, parentFolderUUID: draggedFolder.parentFolderUUID, children: draggedFolder.children) guard let draggedNode = treeController.node(representing: folder) else { return false diff --git a/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift b/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift index ffc3eb9a41..f99f1e040d 100644 --- a/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift +++ b/DuckDuckGo/Bookmarks/Model/PasteboardFolder.swift @@ -26,12 +26,15 @@ struct PasteboardFolder: Hashable { static let name = "name" } - let id: String - let name: String + var id: String { folder.id } + var name: String { folder.title } + var parentFolderUUID: String? { folder.parentFolderUUID } + var children: [BaseBookmarkEntity] { folder.children } - init(id: String, name: String) { - self.id = id - self.name = name + private let folder: BookmarkFolder + + init(folder: BookmarkFolder) { + self.folder = folder } // MARK: - Pasteboard Restoration @@ -41,7 +44,7 @@ struct PasteboardFolder: Hashable { return nil } - self.init(id: id, name: name) + self.init(folder: .init(id: id, title: name)) } init?(pasteboardItem: NSPasteboardItem) { @@ -78,19 +81,17 @@ struct PasteboardFolder: Hashable { static let folderUTIInternalType = NSPasteboard.PasteboardType(rawValue: folderUTIInternal) var pasteboardFolder: PasteboardFolder { - return PasteboardFolder(id: folderID, name: folderName) + return PasteboardFolder(folder: folder) } var internalDictionary: PasteboardAttributes { return pasteboardFolder.internalDictionaryRepresentation } - private let folderID: String - private let folderName: String + private let folder: BookmarkFolder init(folder: BookmarkFolder) { - self.folderID = folder.id - self.folderName = folder.title + self.folder = folder } // MARK: - NSPasteboardWriting @@ -102,7 +103,7 @@ struct PasteboardFolder: Hashable { func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { switch type { case .string: - return folderName + return folder.title case FolderPasteboardWriter.folderUTIInternalType: return internalDictionary default: diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 4830f8d2c2..49ba816dd8 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -314,6 +314,9 @@ final class BookmarkListViewController: NSViewController { FolderPasteboardWriter.folderUTIInternalType]) bookmarkManager.listPublisher.receive(on: DispatchQueue.main).sink { [weak self] list in + list?.bookmarks().forEach({ item in + print("UPDATING LIST - ID: \(item.id) TITLE: \(item.title) URL: \(item.url) ISFAVORITE: \(item.isFavorite)") + }) self?.reloadData() let isEmpty = list?.topLevelEntities.isEmpty ?? true self?.emptyState.isHidden = !isEmpty diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift index 66620ee60a..0aa03eade1 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift @@ -132,24 +132,17 @@ private extension AddEditBookmarkDialogViewModel { func bind() { folderCancellable = bookmarkManager.listPublisher .receive(on: DispatchQueue.main) - .sink(receiveValue: { bookmarkList in - self.folders = .init(bookmarkList) + .sink(receiveValue: { [weak self] bookmarkList in + self?.folders = .init(bookmarkList) }) } func updateBookmark(_ bookmark: Bookmark, url: URL, name: String, isFavorite: Bool, location: BookmarkFolder?) { - var bookmark = bookmark - - // If URL changed update URL first as updating the Bookmark altogether will throw an error as the bookmark can't be fetched by URL. - if bookmark.url != url.absoluteString { - bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark - } - // If the title or isFavorite changed, update the Bookmark - if bookmark.title != name || bookmark.isFavorite != isBookmarkFavorite { - bookmark.title = name - bookmark.isFavorite = isBookmarkFavorite - bookmarkManager.update(bookmark: bookmark) + // If the URL or Title or Favorite is changed update bookmark + if bookmark.url != url.absoluteString || bookmark.title != name || bookmark.isFavorite != isBookmarkFavorite { + bookmarkManager.update(bookmark: bookmark, withURL: url, title: name, isFavorite: isFavorite) } + // If the bookmark changed parent location, move it. if shouldMove(bookmark: bookmark) { let parentFolder: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift index 1c82b4c576..585b30476e 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift @@ -160,6 +160,10 @@ final class BookmarksBarViewModel: NSObject { } self.bookmarksBarItems = displayableItems + bookmarksBarItems.forEach({ item in + guard let bookmark = item.entity as? Bookmark else { return } + print("UPDATING BAR - ID: \(bookmark.id) TITLE: \(bookmark.title) URL: \(bookmark.url) ISFAVORITE: \(bookmark.isFavorite)") + }) if let clippedItemsStartingIndex = clippedItemsStartingIndex { let clippedEntities = bookmarkEntities[clippedItemsStartingIndex...] diff --git a/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift b/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift new file mode 100644 index 0000000000..068d11c9ff --- /dev/null +++ b/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift @@ -0,0 +1,223 @@ +// +// BaseBookmarkEntityTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class BaseBookmarkEntityTests: XCTestCase { + + // MARK: - Folders + + func testTwoBookmarkFolderWithSamePropertiesReturnTrueWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkFolderWithDifferentIdReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkFolderWithDifferentTitleReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child 1", parentFolderUUID: parentFolder.id, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkFolderWithDifferentParentReturnFalseWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: parentFolder.id, children: []) + let rhs = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: #function, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkParentFolderWithSameSubfoldersReturnTrueWhenIsEqualCalled() { + // GIVEN + let folder1 = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: "Parent", children: []) + let folder2 = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: "Parent", children: []) + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkParentFolderWithDifferentSubfoldersReturnFalseWhenIsEqualCalled() { + // GIVEN + let folder1 = BookmarkFolder(id: "1", title: "Child", parentFolderUUID: "Parent", children: []) + let folder2 = BookmarkFolder(id: "2", title: "Child", parentFolderUUID: "Parent", children: []) + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [folder1, folder2, BookmarkFolder(id: "3", title: "")]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkParentFolderWithSameBookmarksReturnTrueWhenIsEqualCalled() { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let bookmark2 = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkParentFolderWithDifferentBookmarksReturnFalseWhenIsEqualCalled() { + // GIVEN + let bookmark1 = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let bookmark2 = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "1-Parent") + let lhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2]) + let rhs = BookmarkFolder(id: "1-Parent", title: "Parent", children: [bookmark1, bookmark2, BookmarkFolder(id: "4", title: "New")]) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Bookmarks + + func testTwoBookmarkWithSamePropertiesReturnTrueWhenIsEqualCalled() { + // GIVEN + let parentFolder = BookmarkFolder(id: "Parent", title: "Parent") + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: parentFolder.id) + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: parentFolder.id) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkWithDifferentIdReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentURLReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.devMode, title: "DDG", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentTitleReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG 2", isFavorite: true, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentIsFavoriteReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "z") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + func testTwoBookmarkWithDifferentParentFolderReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true, parentFolderUUID: "z") + let rhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false, parentFolderUUID: "z-a") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Base Entity + + func testDifferentBookmarkEntitiesReturnFalseWhenIsEqualCalled() { + // GIVEN + let lhs = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhs = BookmarkFolder(id: "1", title: "DDG") + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertFalse(result) + } + +} diff --git a/UnitTests/Bookmarks/Model/BookmarkListTests.swift b/UnitTests/Bookmarks/Model/BookmarkListTests.swift index e959ff9878..f0f8cda903 100644 --- a/UnitTests/Bookmarks/Model/BookmarkListTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkListTests.swift @@ -23,15 +23,20 @@ import XCTest final class BookmarkListTests: XCTestCase { - func testWhenBookmarkIsInserted_ThenItIsPartOfTheList() { + func testWhenBookmarkIsInserted_ThenItIsPartOfTheList() throws { var bookmarkList = BookmarkList() let bookmark = Bookmark.aBookmark bookmarkList.insert(bookmark) + let result = try XCTUnwrap(bookmarkList[bookmark.url]) XCTAssert(bookmarkList.bookmarks().count == 1) XCTAssert((bookmarkList.bookmarks()).first == bookmark.identifiableBookmark) - XCTAssertNotNil(bookmarkList[bookmark.url]) + XCTAssertEqual(result.id, bookmark.id) + XCTAssertEqual(result.title, bookmark.title) + XCTAssertEqual(result.url, bookmark.url) + XCTAssertEqual(result.isFavorite, bookmark.isFavorite) + XCTAssertEqual(result.parentFolderUUID, bookmark.parentFolderUUID) } func testWhenBookmarkIsAlreadyPartOfTheListInserted_ThenItCantBeInserted() { @@ -93,7 +98,7 @@ final class BookmarkListTests: XCTestCase { XCTAssertNil(updateUrlResult) } - func testWhenBookmarkUrlIsUpdated_ThenJustTheBookmarkUrlIsUpdated() { + func testWhenBookmarkUrlIsUpdated_ThenJustTheBookmarkUrlIsUpdated() throws { var bookmarkList = BookmarkList() let bookmarks = [ @@ -104,11 +109,14 @@ final class BookmarkListTests: XCTestCase { bookmarks.forEach { bookmarkList.insert($0) } let bookmarkToReplace = bookmarks[2] - let newBookmark = bookmarkList.updateUrl(of: bookmarkToReplace, to: URL.duckDuckGoAutocomplete.absoluteString) + let newBookmark = try XCTUnwrap(bookmarkList.updateUrl(of: bookmarkToReplace, to: URL.duckDuckGoAutocomplete.absoluteString)) + let result = try XCTUnwrap(bookmarkList[newBookmark.url]) XCTAssert(bookmarkList.bookmarks().count == bookmarks.count) XCTAssertNil(bookmarkList[bookmarkToReplace.url]) - XCTAssertNotNil(bookmarkList[newBookmark!.url]) + XCTAssertEqual(result.title, "Title") + XCTAssertEqual(result.url, URL.duckDuckGoAutocomplete.absoluteString) + XCTAssertTrue(result.isFavorite) } func testWhenBookmarkUrlIsUpdatedToAlreadyBookmarkedUrl_ThenUpdatingMustFail() { @@ -127,10 +135,51 @@ final class BookmarkListTests: XCTestCase { XCTAssert(bookmarkList.bookmarks().count == bookmarks.count) XCTAssertNotNil(bookmarkList[firstUrl.absoluteString]) + XCTAssertEqual(bookmarkList[firstUrl.absoluteString]?.url, firstUrl.absoluteString) XCTAssertNotNil(bookmarkList[bookmarkToReplace.url]) + XCTAssertEqual(bookmarkList[bookmarkToReplace.url]?.url, URL.duckDuckGo.absoluteString) XCTAssertNil(newBookmark) } + func testWhenBookmarkURLTitleAndIsFavoriteIsUpdated_ThenURLTitleAndIsFavoriteIsUpdated() throws { + // GIVEN + var bookmarkList = BookmarkList() + let bookmarks = [ + Bookmark(id: UUID().uuidString, url: "wikipedia.org", title: "Wikipedia", isFavorite: true), + Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true), + Bookmark(id: UUID().uuidString, url: "apple.com", title: "Apple", isFavorite: true) + ] + bookmarks.forEach { bookmarkList.insert($0) } + let bookmarkToReplace = bookmarks[2] + XCTAssertEqual(bookmarkList.bookmarks().count, bookmarks.count) + XCTAssertEqual(bookmarkList["wikipedia.org"]?.url, "wikipedia.org") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.title, "Wikipedia") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.isFavorite, true) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.title, "DDG") + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.isFavorite, true) + XCTAssertEqual(bookmarkList["apple.com"]?.url, "apple.com") + XCTAssertEqual(bookmarkList["apple.com"]?.title, "Apple") + XCTAssertEqual(bookmarkList["apple.com"]?.isFavorite, true) + + // WHEN + let newBookmark = try XCTUnwrap(bookmarkList.update(bookmark: bookmarkToReplace, newURL: "www.example.com", newTitle: "Example", newIsFavorite: false)) + + // THEN + let result = try XCTUnwrap(bookmarkList[newBookmark.url]) + XCTAssertEqual(bookmarkList.bookmarks().count, bookmarks.count) + XCTAssertNil(bookmarkList[bookmarkToReplace.url]) + XCTAssertEqual(bookmarkList["wikipedia.org"]?.url, "wikipedia.org") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.title, "Wikipedia") + XCTAssertEqual(bookmarkList["wikipedia.org"]?.isFavorite, true) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.title, "DDG") + XCTAssertEqual(bookmarkList[URL.duckDuckGo.absoluteString]?.isFavorite, true) + XCTAssertEqual(result.url, "www.example.com") + XCTAssertEqual(result.title, "Example") + XCTAssertEqual(result.isFavorite, false) + } + } fileprivate extension Bookmark { diff --git a/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift b/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift index 8a9060c2f9..fd795b2ebb 100644 --- a/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkNodeTests.swift @@ -217,4 +217,174 @@ class BookmarkNodeTests: XCTestCase { XCTAssertNotEqual(rootNode.findOrCreateChildNode(with: TestObject()), childNode) } + // MARK: - Equality Bookmarks + + func testWhenTwoNodesWithSameIdAndSameBookmarkAsRepresentedObject_ThenIsEqualShouldBeTrue() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertTrue(result) + } + + func testWhenTwoNodesWithDifferentIdAndSameBookmarkAsRepresentedObject_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 2) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentId_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentURL_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.ddgLearnMore.absoluteString, title: "DDG", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentTitle_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "2", url: URL.duckDuckGo.absoluteString, title: "DDG 2", isFavorite: true) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentIsFavorite_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true) + let rhsBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false) + let lhsNode = BookmarkNode(representedObject: lhsBookmark, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsBookmark, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesWithSameIdAndSameFolderAsRepresentedObject_ThenIsEqualShouldBeTrue() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertTrue(result) + } + + func testWhenTwoNodesWithDifferentIdAndSameFolderAsRepresentedObject_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 2) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentId_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "2", title: "Folder", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentName_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder 1", parentFolderUUID: "1", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainBookmarkAsRepresentedObjectWithDifferentParentFolder_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: []) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "2", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + + func testWhenTwoNodesContainFolderAsRepresentedObjectWithDifferentChildren_ThenIsEqualShouldBeFalse() { + // GIVEN + let lhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "1", children: [Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: true)]) + let rhsFolder = BookmarkFolder(id: "1", title: "Folder", parentFolderUUID: "2", children: []) + let lhsNode = BookmarkNode(representedObject: lhsFolder, parent: nil, uniqueId: 1) + let rhsNode = BookmarkNode(representedObject: rhsFolder, parent: nil, uniqueId: 1) + + // WHEN + let result = lhsNode == rhsNode + + // THEN + XCTAssertFalse(result) + } + } diff --git a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift index 2ec8d590de..d1cc5fa4d8 100644 --- a/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift @@ -110,7 +110,7 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let mockDestinationNode = treeController.node(representing: mockDestinationFolder)! let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) - let pasteboardFolder = PasteboardFolder(id: UUID().uuidString, name: "Pasteboard Folder") + let pasteboardFolder = PasteboardFolder(folder: .init(id: UUID().uuidString, title: "Pasteboard Folder")) let result = dataSource.validateDrop(for: [pasteboardFolder], destination: mockDestinationNode) XCTAssertEqual(result, .move) @@ -130,7 +130,7 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let dataSource = BookmarkOutlineViewDataSource(contentMode: .foldersOnly, bookmarkManager: bookmarkManager, treeController: treeController) let mockDestinationNode = treeController.node(representing: mockDestinationFolder)! - let pasteboardFolder = PasteboardFolder(id: mockDestinationFolder.id, name: "Pasteboard Folder") + let pasteboardFolder = PasteboardFolder(folder: mockDestinationFolder) let result = dataSource.validateDrop(for: [pasteboardFolder], destination: mockDestinationNode) XCTAssertEqual(result, .none) @@ -153,7 +153,7 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let mockDestinationNode = treeController.node(representing: childFolder)! // Simulate dragging the root folder onto the child folder: - let draggedFolder = PasteboardFolder(id: rootFolder.id, name: "Root") + let draggedFolder = PasteboardFolder(folder: rootFolder) let result = dataSource.validateDrop(for: [draggedFolder], destination: mockDestinationNode) XCTAssertEqual(result, .none) diff --git a/UnitTests/Bookmarks/Model/BookmarkTests.swift b/UnitTests/Bookmarks/Model/BookmarkTests.swift index cd21752a06..e6cf1df214 100644 --- a/UnitTests/Bookmarks/Model/BookmarkTests.swift +++ b/UnitTests/Bookmarks/Model/BookmarkTests.swift @@ -90,7 +90,7 @@ class BookmarkTests: XCTestCase { XCTAssertEqual(folder.childFolders.count, 0) XCTAssertEqual(folder.childBookmarks.count, 1) XCTAssertEqual(folder.children, [ - BaseBookmarkEntity.from(managedObject: bookmarkManagedObject, favoritesDisplayMode: .displayNative(.desktop)) + BaseBookmarkEntity.from(managedObject: bookmarkManagedObject, parentFolderUUID: folder.id, favoritesDisplayMode: .displayNative(.desktop)) ]) XCTAssertNil(folder.parentFolderUUID) diff --git a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift index 12812e9935..2ebff62e7d 100644 --- a/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/UnitTests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -56,7 +56,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let savingExpectation = self.expectation(description: "Saving") let loadingExpectation = self.expectation(description: "Loading") - let bookmark = Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + let bookmark = Bookmark(id: UUID().uuidString, url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "bookmarks_root") bookmarkStore.save(bookmark: bookmark, parent: nil, index: nil) { (success, error) in XCTAssert(success) @@ -128,7 +128,7 @@ final class LocalBookmarkStoreTests: XCTestCase { savingExpectation.fulfill() - let modifiedBookmark = Bookmark(id: bookmark.id, url: URL.duckDuckGo.absoluteString, title: "New Title", isFavorite: false) + let modifiedBookmark = Bookmark(id: bookmark.id, url: URL.duckDuckGo.absoluteString, title: "New Title", isFavorite: false, parentFolderUUID: "bookmarks_root") bookmarkStore.update(bookmark: modifiedBookmark) bookmarkStore.loadAll(type: .bookmarks) { bookmarks, error in @@ -152,7 +152,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let savingExpectation = self.expectation(description: "Saving") let loadingExpectation = self.expectation(description: "Loading") - let folder = BookmarkFolder(id: UUID().uuidString, title: "Folder") + let folder = BookmarkFolder(id: UUID().uuidString, title: "Folder", parentFolderUUID: "bookmarks_root") bookmarkStore.save(folder: folder, parent: nil) { (success, error) in XCTAssert(success) @@ -181,8 +181,9 @@ final class LocalBookmarkStoreTests: XCTestCase { let saveChildExpectation = self.expectation(description: "Save Child Folder") let loadingExpectation = self.expectation(description: "Loading") - let parentFolder = BookmarkFolder(id: UUID().uuidString, title: "Parent") - let childFolder = BookmarkFolder(id: UUID().uuidString, title: "Child") + let parentId = UUID().uuidString + let childFolder = BookmarkFolder(id: UUID().uuidString, title: "Child", parentFolderUUID: parentId) + let parentFolder = BookmarkFolder(id: parentId, title: "Parent", parentFolderUUID: "bookmarks_root", children: [childFolder]) bookmarkStore.save(folder: parentFolder, parent: nil) { (success, error) in XCTAssert(success) @@ -224,8 +225,9 @@ final class LocalBookmarkStoreTests: XCTestCase { let saveBookmarkExpectation = self.expectation(description: "Save Bookmark") let loadingExpectation = self.expectation(description: "Loading") - let folder = BookmarkFolder(id: UUID().uuidString, title: "Parent") - let bookmark = Bookmark(id: UUID().uuidString, url: "https://example.com", title: "Example", isFavorite: false) + let parentId = UUID().uuidString + let bookmark = Bookmark(id: UUID().uuidString, url: "https://example.com", title: "Example", isFavorite: false, parentFolderUUID: parentId) + let folder = BookmarkFolder(id: parentId, title: "Parent", parentFolderUUID: "bookmarks_root", children: [bookmark]) bookmarkStore.save(folder: folder, parent: nil) { (success, error) in XCTAssert(success) @@ -471,7 +473,7 @@ final class LocalBookmarkStoreTests: XCTestCase { func testWhenUpdatingBookmarkFolder_ThenBookmarkFolderTitleIsUpdated() async throws { let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) - let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1") + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") // Save the initial bookmarks state: @@ -502,9 +504,9 @@ final class LocalBookmarkStoreTests: XCTestCase { let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) - let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1") - let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2") - let folder3 = BookmarkFolder(id: UUID().uuidString, title: "Folder 3") + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2", parentFolderUUID: "bookmarks_root") + let folder3 = BookmarkFolder(id: UUID().uuidString, title: "Folder 3", parentFolderUUID: "bookmarks_root") // Save the initial bookmarks state: @@ -526,6 +528,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let folderToMove = folder1 folderToMove.title = #function bookmarkStore.update(folder: folder1, andMoveToParent: .parent(uuid: folder2.id)) + let expectedFolderAfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: folder2.id, children: folder1.children) // Check the new bookmark folders order: @@ -533,7 +536,7 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(newFolders.count, 2) XCTAssertEqual(newFolders[0].id, folder2.id) - XCTAssertEqual(newFolders[0].children, [folder1]) + XCTAssertEqual(newFolders[0].children, [expectedFolderAfterMove]) XCTAssertEqual(newFolders[1], folder3) } @@ -541,8 +544,8 @@ final class LocalBookmarkStoreTests: XCTestCase { let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) - let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1") - let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2") + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: "bookmarks_root") + let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2", parentFolderUUID: "bookmarks_root") // Save the initial bookmarks state: @@ -560,22 +563,25 @@ final class LocalBookmarkStoreTests: XCTestCase { // Update the folder parent: _ = await bookmarkStore.move(objectUUIDs: [folder2.id], toIndex: nil, withinParentFolder: .parent(uuid: folder1.id)) + let expectedChildFolderAfterMove = BookmarkFolder(id: folder2.id, title: folder2.title, parentFolderUUID: folder1.id, children: folder2.children) + let expectedParentFolderAfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: folder1.parentFolderUUID, children: [expectedChildFolderAfterMove]) // Check the new bookmark folders order: let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } XCTAssertEqual(newFolders.count, 1) - XCTAssertEqual(newFolders.first, folder1) - XCTAssertEqual(newFolders.first?.children, [folder2]) + XCTAssertEqual(newFolders.first, expectedParentFolderAfterMove) + XCTAssertEqual(newFolders.first?.children, [expectedChildFolderAfterMove]) } func testWhenMovingBookmarkFolderToRootFolder_ThenBookmarkFolderLocationIsUpdated() async throws { let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) - let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1") - let folder2 = BookmarkFolder(id: UUID().uuidString, title: "Folder 2") + let folder2Id = UUID().uuidString + let folder1 = BookmarkFolder(id: UUID().uuidString, title: "Folder 1", parentFolderUUID: folder2Id) + let folder2 = BookmarkFolder(id: folder2Id, title: "Folder 2", parentFolderUUID: "bookmarks_root", children: [folder1]) // Save the initial bookmarks state: @@ -597,10 +603,12 @@ final class LocalBookmarkStoreTests: XCTestCase { // Check the new bookmark folders order: let newFolders = try await bookmarkStore.loadAll(type: .topLevelEntities).get().compactMap { $0 as? BookmarkFolder } + let expectedFolder1AfterMove = BookmarkFolder(id: folder1.id, title: folder1.title, parentFolderUUID: "bookmarks_root", children: folder1.children) + let expectedFolder2AfterMove = BookmarkFolder(id: folder2.id, title: folder2.title, parentFolderUUID: "bookmarks_root", children: []) XCTAssertEqual(newFolders.count, 2) - XCTAssertEqual(newFolders.first, folder1) - XCTAssertEqual(newFolders.last, folder2) + XCTAssertEqual(newFolders.first, expectedFolder1AfterMove) + XCTAssertEqual(newFolders.last, expectedFolder2AfterMove) XCTAssertEqual(newFolders.last?.children, []) } diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift index d564fb4064..b409df5b9c 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -484,10 +484,10 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) } - func testShouldAskBookmarkStoreToUpdateURLWhenURLIsUpdatedAndModeIsEdit() { + func testShouldAskBookmarkStoreToUpdateBookmarkWhenURLIsUpdatedAndModeIsEdit() { // GIVEN let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) - let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: "DuckDuckGo", isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: #function, isFavorite: false) let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) sut.bookmarkURLPath = expectedBookmark.url bookmarkStoreMock.bookmarks = [bookmark] @@ -507,7 +507,7 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) } - func testShouldNotAskBookmarkStoreToUpdateURLWhenURLIsNotUpdatedAndModeIsEdit() { + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenURLIsNotUpdatedAndModeIsEdit() { // GIVEN let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) @@ -577,7 +577,7 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { func testShouldAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsUpdatedAndModeIsEdit() { // GIVEN let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) - let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: true) let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) bookmarkStoreMock.bookmarks = [bookmark] bookmarkManager.loadBookmarks() @@ -619,6 +619,31 @@ final class AddEditBookmarkDialogViewModelTests: XCTestCase { XCTAssertNil(bookmarkStoreMock.capturedBookmark) } + func testShouldAskBookmarkStoreToUpdateBookmarkWhenURLAndTitleAndIsFavoriteIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: "DDG", isFavorite: true) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkURLPath = expectedBookmark.url + sut.bookmarkName = expectedBookmark.title + sut.isBookmarkFavorite = expectedBookmark.isFavorite + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + func testShouldAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsDifferentFromOriginalFolderAndModeIsEdit() { // GIVEN let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder") diff --git a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift index 0d719e1a87..28f65fdf59 100644 --- a/UnitTests/HomePage/Mocks/MockBookmarkManager.swift +++ b/UnitTests/HomePage/Mocks/MockBookmarkManager.swift @@ -59,6 +59,8 @@ class MockBookmarkManager: BookmarkManager { func update(bookmark: DuckDuckGo_Privacy_Browser.Bookmark) {} + func update(bookmark: DuckDuckGo_Privacy_Browser.Bookmark, withURL url: URL, title: String, isFavorite: Bool) {} + func update(folder: DuckDuckGo_Privacy_Browser.BookmarkFolder) {} func update(folder: DuckDuckGo_Privacy_Browser.BookmarkFolder, andMoveToParent parent: DuckDuckGo_Privacy_Browser.ParentFolderType) {} From 3c941e6a83dfeedc5401863082af4ba59b1975cb Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 7 Mar 2024 19:13:42 +1100 Subject: [PATCH 11/19] Fix merge conflicts when rebasing --- DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift | 2 +- DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift index a50cc0ee06..603849bbbf 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift @@ -103,7 +103,7 @@ final class BookmarkOutlineCellView: NSTableCellView { countLabel.lineBreakMode = .byClipping menuButton.translatesAutoresizingMaskIntoConstraints = false - menuButton.contentTintColor = .buttonColor + menuButton.contentTintColor = .button menuButton.imagePosition = .imageTrailing menuButton.isBordered = false menuButton.isHidden = true diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift index cc7577766f..a14e57e521 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift @@ -96,7 +96,6 @@ final class BookmarkTableCellView: NSTableCellView { addSubview(containerView) - containerView.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(faviconImageView) containerView.addSubview(titleLabel) From c53b8fd7b9c197b58804748a3286da1fab3045bd Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 7 Mar 2024 19:23:53 +1100 Subject: [PATCH 12/19] Remove stale bookmark string not used anymore --- DuckDuckGo/Localizable.xcstrings | 173 ------------------------------- 1 file changed, 173 deletions(-) diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 47bd4f9d1c..c372a24dcb 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -908,66 +908,6 @@ } } }, - "add.folder.name" : { - "comment" : "Add Folder popover: folder name text field title", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Name:" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Name:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombre:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nom :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naam:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nazwa:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Имя:" - } - } - } - }, "add.link.to.bookmarks" : { "comment" : "Context menu item", "extractionState" : "extracted_with_value", @@ -15626,66 +15566,6 @@ } } }, - "edit.folder" : { - "comment" : "Header of the view that edits a bookmark folder", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ordner bearbeiten" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Edit Folder" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar carpeta" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifier le dossier" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modifica cartella" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Map bewerken" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edytuj folder" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Editar pasta" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изменить папку" - } - } - } - }, "email.copied" : { "comment" : "Notification that the Private email address was copied to clipboard after the user generated a new address", "extractionState" : "extracted_with_value", @@ -24584,59 +24464,6 @@ } } }, - "Location:" : { - "comment" : "Add Folder popover: parent folder picker title", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standort:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubicación:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Emplacement :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Posizione:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Locatie:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lokalizacja:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Localização:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Папка:" - } - } - } - }, "looking.for.bitwarden" : { "comment" : "Setup of the integration with Bitwarden app", "extractionState" : "extracted_with_value", From 1a43a18782eadea5a99737c8713e36ee9f622b5c Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 8 Mar 2024 09:28:21 +1100 Subject: [PATCH 13/19] Bookmarks remove unused code (#2323) Task/Issue URL: https://app.asana.com/0/0/1206772004955801/f **Description**: Remove old Bookmarks Views and View Models not used anymore. --- DuckDuckGo.xcodeproj/project.pbxproj | 32 ----- .../View/AddBookmarkFolderModalView.swift | 67 --------- .../Bookmarks/View/AddBookmarkModalView.swift | 77 ----------- .../Dialog/AddEditBookmarkDialogView.swift | 26 ++-- .../AddEditBookmarkFolderDialogView.swift | 27 ++-- .../AddBookmarkFolderModalViewModel.swift | 79 ----------- .../ViewModel/AddBookmarkModalViewModel.swift | 127 ------------------ DuckDuckGo/Localizable.xcstrings | 53 -------- 8 files changed, 21 insertions(+), 467 deletions(-) delete mode 100644 DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift delete mode 100644 DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift delete mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift delete mode 100644 DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 54baf99a04..7c187a6bc7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -268,7 +268,6 @@ 3706FADC293F65D500E42796 /* FirefoxLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93826B48A5100879451 /* FirefoxLoginReader.swift */; }; 3706FADD293F65D500E42796 /* AtbParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50382726A12400758A2B /* AtbParser.swift */; }; 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; - 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929426670D2A00AD2C21 /* BookmarkSidebarTreeController.swift */; }; 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E8627BBB8F20038AD11 /* HomePageFavoritesModel.swift */; }; 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -387,7 +386,6 @@ 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; }; 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DF326B0002B00E14D75 /* SecureVaultLoginImporter.swift */; }; - 3706FB71293F65D500E42796 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C1DD4285C780C0089850C /* RecentlyClosedCoordinator.swift */; }; 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6197C5276B3168008396F0 /* FaviconHostReference.swift */; }; 3706FB76293F65D500E42796 /* ASN1Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93A26B48ADF00879451 /* ASN1Parser.swift */; }; @@ -1279,8 +1277,6 @@ 4B9292CF2667123700AD2C21 /* BookmarkManagementSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C72667123700AD2C21 /* BookmarkManagementSidebarViewController.swift */; }; 4B9292D02667123700AD2C21 /* BookmarkManagementSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C82667123700AD2C21 /* BookmarkManagementSplitViewController.swift */; }; 4B9292D12667123700AD2C21 /* BookmarkTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */; }; - 4B9292D22667123700AD2C21 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; - 4B9292D32667123700AD2C21 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 4B9292D52667123700AD2C21 /* BookmarkManagementDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CD2667123700AD2C21 /* BookmarkManagementDetailViewController.swift */; }; 4B9292D92667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292D82667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift */; }; @@ -1391,7 +1387,6 @@ 4B9579B52AC7AE700062CA31 /* FirefoxLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AC93826B48A5100879451 /* FirefoxLoginReader.swift */; }; 4B9579B62AC7AE700062CA31 /* AtbParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50382726A12400758A2B /* AtbParser.swift */; }; 4B9579B72AC7AE700062CA31 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; - 4B9579B82AC7AE700062CA31 /* AddBookmarkFolderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */; }; 4B9579B92AC7AE700062CA31 /* BookmarkSidebarTreeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929426670D2A00AD2C21 /* BookmarkSidebarTreeController.swift */; }; 4B9579BA2AC7AE700062CA31 /* HomePageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E8627BBB8F20038AD11 /* HomePageFavoritesModel.swift */; }; 4B9579BB2AC7AE700062CA31 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */; }; @@ -1561,7 +1556,6 @@ 4B957A692AC7AE700062CA31 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; }; 4B957A6A2AC7AE700062CA31 /* SecureVaultLoginImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723DF326B0002B00E14D75 /* SecureVaultLoginImporter.swift */; }; 4B957A6B2AC7AE700062CA31 /* WKProcessPoolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */; }; - 4B957A6C2AC7AE700062CA31 /* AddBookmarkModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */; }; 4B957A6D2AC7AE700062CA31 /* LoginItemsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AE86A2AA76CF90026E7DC /* LoginItemsManager.swift */; }; 4B957A6E2AC7AE700062CA31 /* PixelExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857E5AF42A79045800FC0FB4 /* PixelExperiment.swift */; }; 4B957A6F2AC7AE700062CA31 /* DuckPlayerTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C416A6294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift */; }; @@ -3101,15 +3095,9 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; - B6F9BDD82B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; - B6F9BDD92B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; - B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */; }; B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; B6F9BDDD2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; B6F9BDDE2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */; }; - B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; - B6F9BDE12B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; - B6F9BDE22B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */; }; B6F9BDE42B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; B6F9BDE52B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; B6F9BDE62B45CD1900677B33 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F9BDE32B45CD1900677B33 /* ModalView.swift */; }; @@ -3748,8 +3736,6 @@ 4B9292C72667123700AD2C21 /* BookmarkManagementSidebarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementSidebarViewController.swift; sourceTree = ""; }; 4B9292C82667123700AD2C21 /* BookmarkManagementSplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementSplitViewController.swift; sourceTree = ""; }; 4B9292C92667123700AD2C21 /* BookmarkTableRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkTableRowView.swift; sourceTree = ""; }; - 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderModalView.swift; sourceTree = ""; }; - 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddBookmarkModalView.swift; sourceTree = ""; }; 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkListViewController.swift; sourceTree = ""; }; 4B9292CD2667123700AD2C21 /* BookmarkManagementDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkManagementDetailViewController.swift; sourceTree = ""; }; 4B9292D82667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerDataSource.swift; sourceTree = ""; }; @@ -4529,9 +4515,7 @@ B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = ""; }; B6F7128029F681EB00594A45 /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; }; - B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBookmarkModalViewModel.swift; sourceTree = ""; }; B6F9BDDB2B45B7EE00677B33 /* WebsiteInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteInfo.swift; sourceTree = ""; }; - B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBookmarkFolderModalViewModel.swift; sourceTree = ""; }; B6F9BDE32B45CD1900677B33 /* ModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalView.swift; sourceTree = ""; }; B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; @@ -7523,8 +7507,6 @@ B69A14F12B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift */, B69A14F52B4D701F00B9417D /* AddBookmarkPopoverViewModel.swift */, AAB549DE25DAB8F80058460B /* BookmarkViewModel.swift */, - B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */, - B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */, 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */, 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */, 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */, @@ -7600,9 +7582,7 @@ isa = PBXGroup; children = ( 9FA173DD2B7A0ECE00EE4E6E /* Dialog */, - 4B9292CA2667123700AD2C21 /* AddBookmarkFolderModalView.swift */, 7BEC20412B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift */, - 4B9292CB2667123700AD2C21 /* AddBookmarkModalView.swift */, AAC5E4C425D6A6E8007F5990 /* AddBookmarkPopover.swift */, 7BEC20402B0F505F00243D3E /* AddBookmarkPopoverView.swift */, B69A14F92B4D705D00B9417D /* BookmarkFolderPicker.swift */, @@ -9966,7 +9946,6 @@ 3706FADE293F65D500E42796 /* PreferencesDuckPlayerView.swift in Sources */, EEC4A65E2B277E8D00F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, B66260E729ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, - 3706FADF293F65D500E42796 /* AddBookmarkFolderModalView.swift in Sources */, 3706FAE0293F65D500E42796 /* BookmarkSidebarTreeController.swift in Sources */, 3706FAE1293F65D500E42796 /* HomePageFavoritesModel.swift in Sources */, 3706FAE2293F65D500E42796 /* SequenceExtensions.swift in Sources */, @@ -10128,7 +10107,6 @@ 3706FB5E293F65D500E42796 /* EncryptionKeyStore.swift in Sources */, 3706FB60293F65D500E42796 /* PasswordManagementIdentityItemView.swift in Sources */, 3706FB61293F65D500E42796 /* ProgressExtension.swift in Sources */, - B6F9BDD92B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 3706FB62293F65D500E42796 /* CSVParser.swift in Sources */, 3706FB64293F65D500E42796 /* PixelDataModel.xcdatamodeld in Sources */, B626A75B29921FAA00053070 /* NavigationActionPolicyExtension.swift in Sources */, @@ -10150,7 +10128,6 @@ 4B4D60C62A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, 3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */, 3706FB70293F65D500E42796 /* SecureVaultLoginImporter.swift in Sources */, - 3706FB71293F65D500E42796 /* AddBookmarkModalView.swift in Sources */, 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */, B69A14F32B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift in Sources */, @@ -10219,7 +10196,6 @@ 3706FB97293F65D500E42796 /* ActionSpeech.swift in Sources */, 3706FB99293F65D500E42796 /* PrivacySecurityPreferences.swift in Sources */, B6AFE6BD29A5D621002FF962 /* HTTPSUpgradeTabExtension.swift in Sources */, - B6F9BDE12B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 3706FB9A293F65D500E42796 /* FireproofDomainsStore.swift in Sources */, 3706FB9B293F65D500E42796 /* PrivacyDashboardPermissionHandler.swift in Sources */, 3706FB9C293F65D500E42796 /* TabCollectionViewModel.swift in Sources */, @@ -11171,7 +11147,6 @@ 4B9579B52AC7AE700062CA31 /* FirefoxLoginReader.swift in Sources */, 4B9579B62AC7AE700062CA31 /* AtbParser.swift in Sources */, 4B9579B72AC7AE700062CA31 /* PreferencesDuckPlayerView.swift in Sources */, - 4B9579B82AC7AE700062CA31 /* AddBookmarkFolderModalView.swift in Sources */, 4B41EDB62B169883001EEDF4 /* VPNFeedbackFormViewController.swift in Sources */, 4B9579B92AC7AE700062CA31 /* BookmarkSidebarTreeController.swift in Sources */, 4B9579BA2AC7AE700062CA31 /* HomePageFavoritesModel.swift in Sources */, @@ -11375,7 +11350,6 @@ 4B957A692AC7AE700062CA31 /* BookmarkListViewController.swift in Sources */, 4B957A6A2AC7AE700062CA31 /* SecureVaultLoginImporter.swift in Sources */, 4B957A6B2AC7AE700062CA31 /* WKProcessPoolExtension.swift in Sources */, - 4B957A6C2AC7AE700062CA31 /* AddBookmarkModalView.swift in Sources */, 4B957A6D2AC7AE700062CA31 /* LoginItemsManager.swift in Sources */, 4B957A6E2AC7AE700062CA31 /* PixelExperiment.swift in Sources */, 4B957A6F2AC7AE700062CA31 /* DuckPlayerTabExtension.swift in Sources */, @@ -11487,7 +11461,6 @@ 4B957AC92AC7AE700062CA31 /* CSVImporter.swift in Sources */, 4B957ACA2AC7AE700062CA31 /* StartupPreferences.swift in Sources */, 4B957ACB2AC7AE700062CA31 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, - B6F9BDE22B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 4B957ACC2AC7AE700062CA31 /* MainMenu.swift in Sources */, 4B957ACE2AC7AE700062CA31 /* BrowserTabViewController.swift in Sources */, 4B957ACF2AC7AE700062CA31 /* CallToAction.swift in Sources */, @@ -11571,7 +11544,6 @@ B66CA4212AD910B300447CF0 /* DataImportView.swift in Sources */, 4B957B162AC7AE700062CA31 /* Downloads.xcdatamodeld in Sources */, 4B957B172AC7AE700062CA31 /* TabPreviewViewController.swift in Sources */, - B6F9BDDA2B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 4B957B182AC7AE700062CA31 /* PreferencesPrivacyView.swift in Sources */, 4B957B192AC7AE700062CA31 /* NSPasteboardExtension.swift in Sources */, 4B957B1A2AC7AE700062CA31 /* OnboardingViewModel.swift in Sources */, @@ -11894,7 +11866,6 @@ 1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */, 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */, 987799F12999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */, - B6F9BDE02B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift in Sources */, 4B8AC93526B3B2FD00879451 /* NSAlert+DataImport.swift in Sources */, AA7412BD24D2BEEE00D22FE0 /* MainWindow.swift in Sources */, AAD6D8882696DF6D002393B3 /* CrashReportPromptViewController.swift in Sources */, @@ -11973,7 +11944,6 @@ 4B8AC93926B48A5100879451 /* FirefoxLoginReader.swift in Sources */, B69B503E2726A12500758A2B /* AtbParser.swift in Sources */, 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */, - 4B9292D22667123700AD2C21 /* AddBookmarkFolderModalView.swift in Sources */, 4B92929E26670D2A00AD2C21 /* BookmarkSidebarTreeController.swift in Sources */, EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 85589E8727BBB8F20038AD11 /* HomePageFavoritesModel.swift in Sources */, @@ -12169,7 +12139,6 @@ 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, 4B723E0D26B0006100E14D75 /* SecureVaultLoginImporter.swift in Sources */, B645D8F629FA95440024461F /* WKProcessPoolExtension.swift in Sources */, - 4B9292D32667123700AD2C21 /* AddBookmarkModalView.swift in Sources */, 9D9AE86B2AA76CF90026E7DC /* LoginItemsManager.swift in Sources */, 857E5AF52A79045800FC0FB4 /* PixelExperiment.swift in Sources */, B6C416A7294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift in Sources */, @@ -12258,7 +12227,6 @@ AA6EF9B525081B4C004754E6 /* MainMenuActions.swift in Sources */, B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */, 4B4D60C02A0C848D00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */, - B6F9BDD82B45B7D900677B33 /* AddBookmarkModalViewModel.swift in Sources */, 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, B6085D092743AAB600A9C456 /* FireproofDomains.xcdatamodeld in Sources */, diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift deleted file mode 100644 index 31cc06dc04..0000000000 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderModalView.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// AddBookmarkFolderModalView.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct AddBookmarkFolderModalView: ModalView { - - @State var model: AddBookmarkFolderModalViewModel = .init() - @Environment(\.dismiss) private var dismiss - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(model.title) - .fontWeight(.semibold) - - HStack(spacing: 16) { - Text(UserText.newBookmarkDialogBookmarkNameTitle) - .frame(height: 22) - - TextField("", text: $model.folderName) - .accessibilityIdentifier("Title Text Field") - .textFieldStyle(.roundedBorder) - .disableAutocorrection(true) - } - .padding(.bottom, 4) - - HStack { - Spacer() - - Button(UserText.cancel) { - model.cancel(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.cancelAction) - - Button(model.addButtonTitle) { - model.addFolder(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.defaultAction) - .disabled(model.isAddButtonDisabled) - - } - } - .font(.system(size: 13)) - .padding() - .frame(width: 450, height: 131) - } - -} - -#Preview { - AddBookmarkFolderModalView() -} diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift deleted file mode 100644 index 56941737fd..0000000000 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkModalView.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// AddBookmarkModalView.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import AppKit -import SwiftUI - -struct AddBookmarkModalView: ModalView { - - @State private(set) var model: AddBookmarkModalViewModel - @Environment(\.dismiss) private var dismiss - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text(model.title) - .fontWeight(.semibold) - - HStack(spacing: 16) { - VStack(alignment: .leading) { - Text("Title:", comment: "Add Bookmark dialog bookmark title field heading") - .frame(height: 22) - Text("Address:", comment: "Add Bookmark dialog bookmark url field heading") - .frame(height: 22) - } - - VStack { - TextField("", text: $model.bookmarkTitle) - .accessibilityIdentifier("Title Text Field") - .textFieldStyle(.roundedBorder) - TextField("", text: $model.bookmarkAddress) - .accessibilityIdentifier("URL Text Field") - .textFieldStyle(.roundedBorder) - .disableAutocorrection(true) - } - } - .padding(.bottom, 4) - - HStack { - Spacer() - - Button(UserText.cancel) { - model.cancel(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.cancelAction) - - Button(model.addButtonTitle) { - model.addOrSave(dismiss: dismiss.callAsFunction) - } - .keyboardShortcut(.defaultAction) - .disabled(model.isAddButtonDisabled) - - } - } - .font(.system(size: 13)) - .padding() - .frame(width: 437, height: 164) - } - -} - -#Preview { - AddBookmarkModalView(model: AddBookmarkModalViewModel()) -} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift index 103a49ed43..2c0256bba4 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -88,20 +88,16 @@ struct AddEditBookmarkDialogView: ModalView { #Preview("Add Bookmark - Light Mode") { let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) bookmarkManager.loadBookmarks() - let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) - let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) - let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) - return AddEditBookmarkDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeAddBookmarkView(parent: nil, bookmarkManager: bookmarkManager) .preferredColorScheme(.light) } -#Preview("Add Bookmark - Light Mode") { +#Preview("Add Bookmark - Dark Mode") { let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) bookmarkManager.loadBookmarks() - let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) - let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) - let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) - return AddEditBookmarkDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeAddBookmarkView(parent: nil, bookmarkManager: bookmarkManager) .preferredColorScheme(.dark) } @@ -110,10 +106,8 @@ struct AddEditBookmarkDialogView: ModalView { let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) bookmarkManager.loadBookmarks() - let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) - let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) - let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) - return AddEditBookmarkDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark, bookmarkManager: bookmarkManager) .preferredColorScheme(.light) } @@ -122,10 +116,8 @@ struct AddEditBookmarkDialogView: ModalView { let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) bookmarkManager.loadBookmarks() - let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) - let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) - let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) - return AddEditBookmarkDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark, bookmarkManager: bookmarkManager) .preferredColorScheme(.dark) } #endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift index f1a9174fa9..f859311335 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift @@ -51,8 +51,8 @@ struct AddEditBookmarkFolderDialogView: ModalView { let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) bookmarkManager.loadBookmarks() - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) - return AddEditBookmarkFolderDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil, bookmarkManager: bookmarkManager) .preferredColorScheme(.light) } @@ -61,8 +61,8 @@ struct AddEditBookmarkFolderDialogView: ModalView { let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) bookmarkManager.loadBookmarks() - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: bookmarkFolder), bookmarkManager: bookmarkManager) - return AddEditBookmarkFolderDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) .preferredColorScheme(.light) } @@ -71,8 +71,8 @@ struct AddEditBookmarkFolderDialogView: ModalView { let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) bookmarkManager.loadBookmarks() - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: bookmarkFolder, parentFolder: nil), bookmarkManager: bookmarkManager) - return AddEditBookmarkFolderDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: bookmarkFolder, parentFolder: nil, bookmarkManager: bookmarkManager) .preferredColorScheme(.light) } @@ -80,9 +80,8 @@ struct AddEditBookmarkFolderDialogView: ModalView { let store = BookmarkStoreMock(bookmarks: []) let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) bookmarkManager.loadBookmarks() - customAssertionFailure = { _, _, _ in } - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) - return AddEditBookmarkFolderDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil, bookmarkManager: bookmarkManager) .preferredColorScheme(.dark) } @@ -91,9 +90,8 @@ struct AddEditBookmarkFolderDialogView: ModalView { let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) bookmarkManager.loadBookmarks() - customAssertionFailure = { _, _, _ in } - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: bookmarkFolder), bookmarkManager: bookmarkManager) - return AddEditBookmarkFolderDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) .preferredColorScheme(.dark) } @@ -102,9 +100,8 @@ struct AddEditBookmarkFolderDialogView: ModalView { let store = BookmarkStoreMock(bookmarks: [bookmarkFolder]) let bookmarkManager = LocalBookmarkManager(bookmarkStore: store) bookmarkManager.loadBookmarks() - customAssertionFailure = { _, _, _ in } - let viewModel = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: bookmarkFolder, parentFolder: bookmarkFolder), bookmarkManager: bookmarkManager) - return AddEditBookmarkFolderDialogView(viewModel: viewModel) + + return BookmarksDialogViewFactory.makeEditBookmarkFolderView(folder: bookmarkFolder, parentFolder: bookmarkFolder, bookmarkManager: bookmarkManager) .preferredColorScheme(.dark) } #endif diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift deleted file mode 100644 index ac2701e3ca..0000000000 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkFolderModalViewModel.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// AddBookmarkFolderModalViewModel.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct AddBookmarkFolderModalViewModel { - - let bookmarkManager: BookmarkManager - - let title: String - let addButtonTitle: String - let originalFolder: BookmarkFolder? - let parent: BookmarkFolder? - - var folderName: String = "" - - var isAddButtonDisabled: Bool { - folderName.trimmingWhitespace().isEmpty - } - - init(folder: BookmarkFolder, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - completionHandler: @escaping (BookmarkFolder?) -> Void = { _ in }) { - self.bookmarkManager = bookmarkManager - self.folderName = folder.title - self.originalFolder = folder - self.parent = nil - self.title = UserText.renameFolder - self.addButtonTitle = UserText.save - } - - init(parent: BookmarkFolder? = nil, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - completionHandler: @escaping (BookmarkFolder?) -> Void = { _ in }) { - self.bookmarkManager = bookmarkManager - self.originalFolder = nil - self.parent = parent - self.title = UserText.newFolder - self.addButtonTitle = UserText.newFolderDialogAdd - } - - func cancel(dismiss: () -> Void) { - dismiss() - } - - func addFolder(dismiss: () -> Void) { - guard !folderName.isEmpty else { - assertionFailure("folderName is empty, button should be disabled") - return - } - - let folderName = folderName.trimmingWhitespace() - if let folder = originalFolder { - folder.title = folderName - bookmarkManager.update(folder: folder) - - } else { - bookmarkManager.makeFolder(for: folderName, parent: parent, completion: { _ in }) - } - - dismiss() - } - -} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift deleted file mode 100644 index 15554afcbc..0000000000 --- a/DuckDuckGo/Bookmarks/ViewModel/AddBookmarkModalViewModel.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// AddBookmarkModalViewModel.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct AddBookmarkModalViewModel { - - let bookmarkManager: BookmarkManager - - let title: String - let addButtonTitle: String - var isFavorite: Bool - - private let originalBookmark: Bookmark? - private let parent: BookmarkFolder? - - private let completionHandler: (Bookmark?) -> Void - - var bookmarkTitle: String = "" - var bookmarkAddress: String = "" - - private var hasValidInput: Bool { - guard let url = bookmarkAddress.url else { return false } - - return !bookmarkTitle.trimmingWhitespace().isEmpty && url.isValid - } - - var isAddButtonDisabled: Bool { !hasValidInput } - - func cancel(dismiss: () -> Void) { - completionHandler(nil) - dismiss() - } - - func addOrSave(dismiss: () -> Void) { - guard let url = bookmarkAddress.url else { - assertionFailure("invalid URL, button should be disabled") - return - } - - var result: Bookmark? - let bookmarkTitle = bookmarkTitle.trimmingWhitespace() - if var bookmark = originalBookmark ?? bookmarkManager.getBookmark(for: url) { - - if url.absoluteString != bookmark.url { - bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark - } - if bookmark.title != bookmarkTitle || bookmark.isFavorite != isFavorite { - bookmark.title = bookmarkTitle - bookmark.isFavorite = isFavorite - bookmarkManager.update(bookmark: bookmark) - } - - result = bookmark - - } else if !bookmarkManager.isUrlBookmarked(url: url) { - result = bookmarkManager.makeBookmark(for: url, title: bookmarkTitle, isFavorite: isFavorite, index: nil, parent: parent) - } - - completionHandler(result) - dismiss() - } - - init(isFavorite: Bool = false, - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - currentTabWebsite website: WebsiteInfo? = nil, - parent: BookmarkFolder? = nil, - completionHandler: @escaping (Bookmark?) -> Void = { _ in }) { - - self.bookmarkManager = bookmarkManager - - self.isFavorite = isFavorite - self.title = isFavorite ? UserText.addFavorite : UserText.newBookmark - self.addButtonTitle = UserText.bookmarkDialogAdd - - if let website, - !LocalBookmarkManager.shared.isUrlBookmarked(url: website.url) { - bookmarkTitle = website.title ?? "" - bookmarkAddress = website.url.absoluteString - } - self.parent = parent - self.originalBookmark = nil - - self.completionHandler = completionHandler - } - - init(bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - originalBookmark: Bookmark?, - isFavorite: Bool = false, - completionHandler: @escaping (Bookmark?) -> Void = { _ in }) { - - self.bookmarkManager = bookmarkManager - - self.isFavorite = isFavorite - if originalBookmark != nil { - self.title = isFavorite ? UserText.editFavorite : UserText.updateBookmark - self.addButtonTitle = UserText.save - } else { - self.title = isFavorite ? UserText.addFavorite : UserText.newBookmark - self.addButtonTitle = UserText.bookmarkDialogAdd - } - - self.parent = nil - self.originalBookmark = originalBookmark - - bookmarkTitle = originalBookmark?.title ?? "" - bookmarkAddress = originalBookmark?.url ?? "" - - self.completionHandler = completionHandler - } - -} diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index c372a24dcb..e85fb1e439 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -47497,59 +47497,6 @@ } } }, - "Title:" : { - "comment" : "Add Bookmark dialog bookmark title field heading", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Título:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titre :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titolo:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Titel:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tytuł:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Título:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Название:" - } - } - } - }, "tooltip.addToFavorites" : { "comment" : "Tooltip for add to favorites button", "extractionState" : "extracted_with_value", From 7d97321e8d6055729d5d8f54ee5baa2d2f9940ce Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 8 Mar 2024 21:37:44 +1100 Subject: [PATCH 14/19] Fix Add Bookmark buttons size (#2353) Task/Issue URL: https://app.asana.com/0/0/1206762730363882/f **Description**: When adding a bookmark from the bookmark receipt the buttons were showing in the wrong size. --- DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift | 2 ++ DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift | 2 +- .../Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift index c283331ced..f15465df36 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkFolderPopoverView.swift @@ -67,6 +67,8 @@ struct AddBookmarkFolderPopoverView: ModalView { #Preview("Add Folder - Dark") { let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bkman.loadBookmarks() + customAssertionFailure = { _, _, _ in } return AddBookmarkFolderPopoverView(model: AddBookmarkFolderPopoverViewModel(bookmarkManager: bkman) { print("CompletionHandler:", $0?.title ?? "") diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index 8ddb38ce2c..fba84b44bc 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -40,7 +40,7 @@ struct AddBookmarkPopoverView: View { private var addBookmarkView: some View { AddEditBookmarkView( title: UserText.Bookmarks.Dialog.Title.addedBookmark, - buttonsState: .compressed, + buttonsState: .expanded, bookmarkName: $model.bookmarkTitle, bookmarkURLPath: nil, isBookmarkFavorite: $model.isBookmarkFavorite, diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift index 40c4b19cc0..d89cfc7c93 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderView.swift @@ -63,7 +63,7 @@ struct AddEditBookmarkFolderView: View { }, bottomSection: { BookmarkDialogButtonsView( - viewState: .compressed, + viewState: .init(buttonsState), otherButtonAction: .init( title: cancelActionTitle, keyboardShortCut: .cancelAction, From 7fb6c5327fdad0a786cb8b820aad0f79b0df4249 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 8 Mar 2024 21:44:28 +1100 Subject: [PATCH 15/19] Alessandro/address bookmarks list fix menu (#2352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206762730363836/f **Description**: I found an issue while doing a smoke test for the Ship Review. The first time a bookmark/folder is added from the bookmarks shortcut panel and we open the menu from the “…” menu button, the menu is disabled. The same is not true if we open the menu by right-clicking on the item. --- DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift | 5 +---- DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 49ba816dd8..1e4d1d307d 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -314,9 +314,6 @@ final class BookmarkListViewController: NSViewController { FolderPasteboardWriter.folderUTIInternalType]) bookmarkManager.listPublisher.receive(on: DispatchQueue.main).sink { [weak self] list in - list?.bookmarks().forEach({ item in - print("UPDATING LIST - ID: \(item.id) TITLE: \(item.title) URL: \(item.url) ISFAVORITE: \(item.isFavorite)") - }) self?.reloadData() let isEmpty = list?.topLevelEntities.isEmpty ?? true self?.emptyState.isHidden = !isEmpty @@ -429,7 +426,7 @@ final class BookmarkListViewController: NSViewController { let row = outlineView.row(for: cell) guard let item = outlineView.item(atRow: row), - let contextMenu = ContextualMenu.menu(for: [item]) + let contextMenu = ContextualMenu.menu(for: [item], target: self) else { return } diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift index 585b30476e..1c82b4c576 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewModel.swift @@ -160,10 +160,6 @@ final class BookmarksBarViewModel: NSObject { } self.bookmarksBarItems = displayableItems - bookmarksBarItems.forEach({ item in - guard let bookmark = item.entity as? Bookmark else { return } - print("UPDATING BAR - ID: \(bookmark.id) TITLE: \(bookmark.title) URL: \(bookmark.url) ISFAVORITE: \(bookmark.isFavorite)") - }) if let clippedItemsStartingIndex = clippedItemsStartingIndex { let clippedEntities = bookmarkEntities[clippedItemsStartingIndex...] From 872b40d0399093070ca18bd9f825ee1ac675b3e0 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 11 Mar 2024 19:24:43 +1100 Subject: [PATCH 16/19] =?UTF-8?q?Ensure=20that=20root=20bookmark=20folder?= =?UTF-8?q?=20comparison=20works=20when=20one=20folder=20has=E2=80=A6=20(#?= =?UTF-8?q?2354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206762730363884/f **Description**: In some cases `BookmarkFolder`s that are added to the root folder have `parentFolderUUID` equal to `nil` in some cases they have it equal to the root identifier `bookmarks_root`. Ensure that equality is satisfied when this happens. This bug started when we changed hashable/equatable. --- DuckDuckGo/Bookmarks/Model/Bookmark.swift | 14 ++++++++++- .../Model/BaseBookmarkEntityTests.swift | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Bookmarks/Model/Bookmark.swift b/DuckDuckGo/Bookmarks/Model/Bookmark.swift index 914fdd28d3..ad3dbf808b 100644 --- a/DuckDuckGo/Bookmarks/Model/Bookmark.swift +++ b/DuckDuckGo/Bookmarks/Model/Bookmark.swift @@ -154,7 +154,7 @@ final class BookmarkFolder: BaseBookmarkEntity { return id == folder.id && title == folder.title && isFolder == folder.isFolder && - parentFolderUUID == folder.parentFolderUUID && + isParentFolderEqual(lhs: parentFolderUUID, rhs: folder.parentFolderUUID) && children == folder.children } @@ -166,6 +166,18 @@ final class BookmarkFolder: BaseBookmarkEntity { hasher.combine(children) } + // In some cases a bookmark folder that is child of the root folder has its `parentFolderUUID` set to `bookmarks_root`. In some other cases is nil. Making sure that comparing a `nil` and a `bookmarks_root` does not return false. Probably would be good idea to remove the optionality of `parentFolderUUID` in the future and set it to `bookmarks_root` when needed. + private func isParentFolderEqual(lhs: String?, rhs: String?) -> Bool { + switch (lhs, rhs) { + case (.none, .none): + return true + case (.some(let lhsValue), .some(let rhsValue)): + return lhsValue == rhsValue + case (.some(let value), .none), (.none, .some(let value)): + return value == "bookmarks_root" + } + } + } final class Bookmark: BaseBookmarkEntity { diff --git a/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift b/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift index 068d11c9ff..11ffefab31 100644 --- a/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift +++ b/UnitTests/Bookmarks/Model/BaseBookmarkEntityTests.swift @@ -206,6 +206,30 @@ final class BaseBookmarkEntityTests: XCTestCase { XCTAssertFalse(result) } + func testTwoBookmarkFoldersAddedToRootFolderReturnTrueWhenLeftParentIsBookmarksRootAndRightIsNil() { + // GIVEN + let lhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: "bookmarks_root", children: []) + let rhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: nil, children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + + func testTwoBookmarkFoldersAddedToRootFolderReturnTrueWhenLeftParentIsNilAndRightParentIsRootBookmarks() { + // GIVEN + let lhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: nil, children: []) + let rhs = BookmarkFolder(id: "1", title: "A", parentFolderUUID: "bookmarks_root", children: []) + + // WHEN + let result = lhs == rhs + + // THEN + XCTAssertTrue(result) + } + // MARK: - Base Entity func testDifferentBookmarkEntitiesReturnFalseWhenIsEqualCalled() { From 72f84ec6ec32d4d23f5525d44e3efd56075c79e0 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Wed, 13 Mar 2024 20:36:20 +1100 Subject: [PATCH 17/19] Alessandro/bookmarks ship review design (#2386) Task/Issue URL: https://app.asana.com/0/0/1206818692920856/f **Description**: Fixed Ship Review Design feedback from Bryan. [BB2/](https://app.asana.com/0/0/1206793110089656/1206816441543512/f) [BB4/](https://app.asana.com/0/0/1206793110089656/1206816441543522/f) [BB5/](https://app.asana.com/0/0/1206793110089656/1206816442741770/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 14 +++++ .../Model/BookmarkOutlineViewDataSource.swift | 2 +- .../Bookmarks/Services/ContextualMenu.swift | 2 +- .../View/BookmarkListViewController.swift | 3 +- ...kmarkManagementSidebarViewController.swift | 14 ++++- .../View/BookmarksBarMenuFactory.swift | 7 +++ .../View/BookmarksBarViewController.swift | 21 +++++-- .../BookmarksBarMenuFactoryTests.swift | 59 +++++++++++++++++++ .../Bookmarks/Model/ContextualMenuTests.swift | 2 +- .../Model/LocalBookmarkManagerTests.swift | 2 +- 10 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7c187a6bc7..61bf0b6ae3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2459,6 +2459,8 @@ 9FA173EB2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; 9FA173EC2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; + 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; + 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; @@ -4056,6 +4058,7 @@ 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; + 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuFactoryTests.swift; sourceTree = ""; }; 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFavoriteView.swift; sourceTree = ""; }; 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkView.swift; sourceTree = ""; }; 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewModel.swift; sourceTree = ""; }; @@ -6750,6 +6753,14 @@ path = Dialog; sourceTree = ""; }; + 9FA75A3C2BA00DF500DA5FA6 /* Factory */ = { + isa = PBXGroup; + children = ( + 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */, + ); + path = Factory; + sourceTree = ""; + }; AA0877B626D515EE00B05660 /* UserAgent */ = { isa = PBXGroup; children = ( @@ -7098,6 +7109,7 @@ isa = PBXGroup; children = ( 9F872D9B2B9058B000138637 /* Extensions */, + 9FA75A3C2BA00DF500DA5FA6 /* Factory */, 9F982F102B82264400231028 /* ViewModels */, AA652CAE25DD8228009059CC /* Model */, AA652CAF25DD822C009059CC /* Services */, @@ -10722,6 +10734,7 @@ 3706FE4A293F661700E42796 /* BookmarkManagedObjectTests.swift in Sources */, EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */, 3706FE4B293F661700E42796 /* BookmarksHTMLImporterTests.swift in Sources */, + 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, 56D145E929E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */, 3706FE4C293F661700E42796 /* CSVParserTests.swift in Sources */, 3706FE4D293F661700E42796 /* OnboardingTests.swift in Sources */, @@ -12595,6 +12608,7 @@ B6656E122B29E3BE008798A1 /* DownloadListStoreMock.swift in Sources */, 37D23780287EFEE200BCE03B /* PinnedTabsManagerTests.swift in Sources */, AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */, + 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, B69B50462726C5C200758A2B /* AtbAndVariantCleanupTests.swift in Sources */, 567DA94529E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, 4B59024C26B38BB800489384 /* ChromiumLoginReaderTests.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index a1479ba73d..93a404fa95 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -30,7 +30,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS @Published var selectedFolders: [BookmarkFolder] = [] let treeController: BookmarkTreeController - var expandedNodesIDs = Set() + private(set) var expandedNodesIDs = Set() private let contentMode: ContentMode private let bookmarkManager: BookmarkManager diff --git a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift index 8447acac0a..f79e265d96 100644 --- a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift @@ -121,7 +121,7 @@ private extension ContextualMenu { deleteBookmarkMenuItem(bookmark: bookmark), moveToEndMenuItem(entity: bookmark, parent: parent), NSMenuItem.separator(), - addFolderMenuItem(folder: nil), + addFolderMenuItem(folder: parent), manageBookmarksMenuItem(), ] } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 1e4d1d307d..c405c23f61 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -342,7 +342,8 @@ final class BookmarkListViewController: NSViewController { } @objc func newFolderButtonClicked(_ sender: AnyObject) { - let view = BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil) + let parentFolder = sender.representedObject as? BookmarkFolder + let view = BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parentFolder) showDialog(view: view) } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index a413548f0e..53e502383b 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -211,6 +211,13 @@ final class BookmarkManagementSidebarViewController: NSViewController { // MARK: NSOutlineView Configuration private func expandAndRestore(selectedNodes: [BookmarkNode]) { + // OutlineView doesn't allow multiple selections so there should be only one selected node at time. + let selectedNode = selectedNodes.first + // As the data source reloaded we need to refresh the previously selected nodes. + // Lets consider the scenario where we add a folder to a subfolder. + // When the folder is added we need to "refresh" the node because the previously selected node folder has changed (it has a child folder now). + var refreshedSelectedNodes: [BookmarkNode] = [] + treeController.visitNodes { node in if let objectID = (node.representedObject as? BaseBookmarkEntity)?.id { if dataSource.expandedNodesIDs.contains(objectID) { @@ -218,6 +225,11 @@ final class BookmarkManagementSidebarViewController: NSViewController { } else { outlineView.collapseItem(node) } + + // Add the node if it contains previously selected folder + if let folder = selectedNode?.representedObject as? BookmarkFolder, folder.id == objectID { + refreshedSelectedNodes.append(node) + } } // Expand the Bookmarks pseudo folder automatically. @@ -226,7 +238,7 @@ final class BookmarkManagementSidebarViewController: NSViewController { } } - restoreSelection(to: selectedNodes) + restoreSelection(to: refreshedSelectedNodes) } private func restoreSelection(to nodes: [BookmarkNode]) { diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift index 56eff5f57a..c8d9f6c016 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarMenuFactory.swift @@ -34,6 +34,13 @@ struct BookmarksBarMenuFactory { menu.addItem(makeMenuItem(prefs)) } + static func addToMenuWithManageBookmarksSection(_ menu: NSMenu, target: AnyObject, addFolderSelector: Selector, manageBookmarksSelector: Selector, prefs: AppearancePreferences = .shared) { + addToMenu(menu, prefs) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: UserText.addFolder, action: addFolderSelector, target: target)) + menu.addItem(NSMenuItem(title: UserText.bookmarksManageBookmarks, action: manageBookmarksSelector, target: target)) + } + private static func makeMenuItem( _ prefs: AppearancePreferences) -> NSMenuItem { let item = NSMenuItem(title: UserText.showBookmarksBar, action: nil, keyEquivalent: "B") item.submenu = NSMenu(items: [ diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift index b91c8df14b..e298a6335e 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift @@ -249,7 +249,7 @@ private extension BookmarksBarViewController { case .deleteEntity: bookmarkManager.remove(bookmark: bookmark) case .addFolder: - showDialog(view: BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: nil)) + addFolder(inParent: nil) case .manageBookmarks: manageBookmarks() } @@ -266,7 +266,7 @@ private extension BookmarksBarViewController { case .deleteEntity: bookmarkManager.remove(folder: folder) case .addFolder: - showDialog(view: BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: folder)) + addFolder(inParent: folder) case .openInNewTab: openAllInNewTabs(folder: folder) case .openInNewWindow: @@ -314,14 +314,22 @@ private extension BookmarksBarViewController { menu.popUp(positioning: nil, at: CGPoint(x: 0, y: view.frame.minY - 7), in: view) } + func addFolder(inParent parent: BookmarkFolder?) { + showDialog(view: BookmarksDialogViewFactory.makeAddBookmarkFolderView(parentFolder: parent)) + } + func showDialog(view: any ModalView) { view.show(in: self.view.window) } - func manageBookmarks() { + @objc func manageBookmarks() { WindowControllersManager.shared.showBookmarksTab() } + @objc func addFolder(sender: NSMenuItem) { + addFolder(inParent: nil) + } + } // MARK: - Menu @@ -330,7 +338,12 @@ extension BookmarksBarViewController: NSMenuDelegate { public func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() - BookmarksBarMenuFactory.addToMenu(menu) + BookmarksBarMenuFactory.addToMenuWithManageBookmarksSection( + menu, + target: self, + addFolderSelector: #selector(addFolder(sender:)), + manageBookmarksSelector: #selector(manageBookmarks) + ) } } diff --git a/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift b/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift new file mode 100644 index 0000000000..df9f76ec4e --- /dev/null +++ b/UnitTests/Bookmarks/Factory/BookmarksBarMenuFactoryTests.swift @@ -0,0 +1,59 @@ +// +// BookmarksBarMenuFactoryTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class BookmarksBarMenuFactoryTests: XCTestCase { + + func testReturnAddFolderAndManageBookmarksWhenAddToMenuWithManageBookmarksSectionIsCalled() { + // GIVEN + let menu = NSMenu(title: "") + let targetMock = BookmarksBarTargetMock() + XCTAssertTrue(menu.items.isEmpty) + + // WHEN + BookmarksBarMenuFactory.addToMenuWithManageBookmarksSection(menu, target: targetMock, addFolderSelector: #selector(targetMock.addFolder(_:)), manageBookmarksSelector: #selector(targetMock.manageBookmarks)) + + // THEN + XCTAssertEqual(menu.items.count, 4) + XCTAssertEqual(menu.items[1].title, "") + XCTAssertNil(menu.items[1].action) + XCTAssertEqual(menu.items[2].title, UserText.addFolder) + XCTAssertEqual(menu.items[2].action, #selector(targetMock.addFolder(_:))) + XCTAssertEqual(menu.items[3].title, UserText.bookmarksManageBookmarks) + XCTAssertEqual(menu.items[3].action, #selector(targetMock.manageBookmarks)) + } + + func testShouldNotReturnAddFolderAndManageBookmarksWhenAddToMenuIsCalled() { + // GIVEN + let menu = NSMenu(title: "") + XCTAssertTrue(menu.items.isEmpty) + + // WHEN + BookmarksBarMenuFactory.addToMenu(menu) + + // THEN + XCTAssertEqual(menu.items.count, 1) + } +} + +private class BookmarksBarTargetMock: NSObject { + @objc func addFolder(_ sender: NSMenuItem) {} + @objc func manageBookmarks() {} +} diff --git a/UnitTests/Bookmarks/Model/ContextualMenuTests.swift b/UnitTests/Bookmarks/Model/ContextualMenuTests.swift index d2122ff244..89da2c186a 100644 --- a/UnitTests/Bookmarks/Model/ContextualMenuTests.swift +++ b/UnitTests/Bookmarks/Model/ContextualMenuTests.swift @@ -141,7 +141,7 @@ final class ContextualMenuTests: XCTestCase { assertMenu(item: items[7], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), representedObject: bookmark) assertMenu(item: items[8], withTitle: UserText.bookmarksBarContextMenuMoveToEnd, selector: #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), representedObject: BookmarkEntityInfo(entity: bookmark, parent: parent)) assertMenu(item: items[9], withTitle: "", selector: nil) // Separator - assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:))) + assertMenu(item: items[10], withTitle: UserText.addFolder, selector: #selector(BookmarkMenuItemSelectors.newFolder(_:)), representedObject: parent) assertMenu(item: items[11], withTitle: UserText.bookmarksManageBookmarks, selector: #selector(BookmarkMenuItemSelectors.manageBookmarks(_:))) } diff --git a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift index 96e1c7d4e3..97ce23202d 100644 --- a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift +++ b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift @@ -171,7 +171,7 @@ final class LocalBookmarkManagerTests: XCTestCase { bookmarkManager.update(folder: folder, andMoveToParent: .parent(uuid: parent.id)) - let topLevelEntities = try XCTUnwrap(bookmarkList?.topLevelEntities) + withExtendedLifetime(cancellable) {} XCTAssertTrue(bookmarkStoreMock.updateFolderAndMoveToParentCalled) XCTAssertEqual(bookmarkStoreMock.capturedFolder, folder) XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: parent.id)) From 8979fd78bf3b58e964207693b9aa17f8078aa3d4 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Thu, 14 Mar 2024 20:14:22 +1100 Subject: [PATCH 18/19] Update Manage Bookmarks chevron icon (#2413) Task/Issue URL: https://app.asana.com/0/0/1206828077023778/f **Description**: Update chevron icon for Manage Bookmarks --- .../Chevron-Medium-Right-16.pdf | Bin 0 -> 1154 bytes .../Contents.json | 15 +++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6ce729e793ccc7620579ee06f9b1fc20a892693e GIT binary patch literal 1154 zcmZXUO>dh(5QgvjE9PP;sgz~E7o;dsV>eaRR&_~lQ4R!e94Z@HAXVC5-&wpY*xrLR zJb2#u;DyC{b$7+7C4>Wm=IJK^xW0y)8{+DXza(PLlP|8`_Aek2=Kfz@-@0N?l9l^Y zl`j8y3(NcYRoRi>1Pid?z!f`>;!PeSK~pU?H&E_L%s3CavL>2p$)seE(M*_>E7&7T zg%m;}Gg4+kz>IL6F=dg2yp*QKaIK*voTi)^!|*0oMx_dNlRl@)>+699eNMU8mysj)-%B~dg~h#T7>3w`h*!!p01(Ci$Y?z$u|!n6WlOb z$GhZUDa~~>l!_FMmb|a)rtje45sil@`SJIcgJfN7T>xL*rYdfmH+b+%I@l-%+>B_7 zMy1?!&1+k_4!pJVAh&Gl-ql#q`+9`yqD2NepqcwUif={ox!Pc>;3vpotg~!hF_)k9 z-yxYPnIk1XU>6HcgD;D|*frbZ)VJ0>69$$X#`nKqG`=B#I2?|S@dxUNB=&iX48wU& zkU%>gqZCz6k)5>qwy1WlCI0DsTH!h_x@PY#;iB4H9+r~iP1A!Pcvx_9U;T5~{qVUc Q+I|=(laeeJAHLoF2V>g(ApigX literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json new file mode 100644 index 0000000000..fa088142ba --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Chevron-Medium-Right-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} From 1ce025ad293a62d0136e6756bc4911e087ca49d4 Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 18 Mar 2024 13:01:08 +1100 Subject: [PATCH 19/19] Fix the chevron icon in Manage Bookmarks (#2439) Task/Issue URL: https://app.asana.com/0/0/1206858401182957/f **Description**: Fix the chevron icon on Manage Bookmarks --- .../Chevron-Medium-Right-16.pdf | Bin 1154 -> 1153 bytes .../Chevron-Next-16.pdf | Bin 1160 -> 0 bytes .../Chevron-Next-16.imageset/Contents.json | 15 --------------- .../View/BookmarkTableCellView.swift | 2 +- 4 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Chevron-Next-16.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Chevron-Medium-Right-16.imageset/Chevron-Medium-Right-16.pdf index 6ce729e793ccc7620579ee06f9b1fc20a892693e..7d7e84fbe4807dd5ba892d914bf166abb2b50894 100644 GIT binary patch delta 509 zcmZ9Iu};H442IPR2_+jl!%3(M2+Q`_cV`ARSP{=qv7mx@g9rv@s_wi5FNiOV(xkT} z`4Z>*e}A=~?axa~fWry9kIM^%nAINxL&@19?tZU&1S%1=1OtnP6vJD0i-3uc%waOI zs9}!Sj4_4TB&rgZu;~Pd5>XBc#Ecokwwt~qR}F^k(a`&EOimOCDoX`|XFMnngfqqJ zuOXHErZATzN8+`U2WKbKoPr2dCXfmen@3aVQ%SX`fU(q8>Td_)+aj|lPR2n6P2!W@ za}rEjt3AgUbZBho`Z4PqE%x0|+}}4hEBSkEzE^kNp`P&G4Ohg(UUZ4F*(oYH?yt|D M&$(N#AD$ln0c`4JP5=M^ delta 509 zcmZ9Jze)r#5XQMfkXt)zi-24qvcpW~PZGgWTk#nV3*o$1ShTSAxYEb)1x>PXU3R+s zlKH-GChwb%&DYCCPk^;te>_}W(sc9?m6h!HeLW~BBQv@L$y+3GZ=FK)>^)~@%*lH< zBcur>WYYQ~L^D!#yN)0dnqj^qF^S^#S*yJxkfN`ZHGP7CiUI!{AlSkHf&UB?JO|<6 z5HPSeast7;8qbMXvVfUPeF&Z^6EMnQOR&Cb3}sA#gn6jwMplpl#~h5ZN^4cHQ1kCP zfo3u7Y@b;RLbW4cSy4+nM~iKr?$@^~{drw~uI|*0zjf%wZ$tBW(o$|hF&~yF$m!qrgG^Kyp8*l zXD<18i|hOOQ&z)oAOYD9ypqe=xTy57D20(aR0gvnj60T-IxD~m8%eDtsz3z3iBy0` zh=jD-_ccSMb?o~%AuUW8%fRBaRSRv53Pa35iAYIfB(3V$1W|y8nZ_V-m`N-Y$o-js zoX)o*h-{!OQaR+&C=!y&4-K_+AVydf8!L&7zNGLwW<0U55G zu65XHMa&K2Do2H>Yg}e;gdiEPl3Ucfoufs=GkAgLcTm80;Mnw;7y`su>4FvdYK&Dd zQi_}Rz9`D3#)n6W9}Vxv-(L>BZMt_o`093fdRxBX1GlLQ(>PIPK+|kEDRFiAT4khkC=@gm<{c1`h;vTTqGZX4{~=l>kJZ$BDo)wJcrI)>Hi H!?(Nt>Sg|1 diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json deleted file mode 100644 index 715e36be33..0000000000 --- a/DuckDuckGo/Assets.xcassets/Images/Chevron-Next-16.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Chevron-Next-16.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift index a14e57e521..8bfdc3c4fb 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift @@ -223,7 +223,7 @@ final class BookmarkTableCellView: NSTableCellView { self.entity = folder faviconImageView.image = .folder - accessoryImageView.image = .chevronNext16 + accessoryImageView.image = .chevronMediumRight16 primaryTitleLabelValue = folder.title tertiaryTitleLabelValue = nil }