Skip to content

Commit 7673d18

Browse files
PM-10084: Move save button into navigation bar (#775)
1 parent 7de1657 commit 7673d18

File tree

77 files changed

+195
-128
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+195
-128
lines changed

BitwardenShared/UI/Auth/Landing/SelfHosted/SelfHostedView.swift

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ struct SelfHostedView: View {
1616
VStack(spacing: 16) {
1717
selfHostedEnvironment
1818
customEnvironment
19-
saveButton
2019
}
2120
.textFieldConfiguration(.url)
2221
.navigationBar(title: Localizations.settings, titleDisplayMode: .inline)
@@ -25,6 +24,10 @@ struct SelfHostedView: View {
2524
cancelToolbarItem {
2625
store.send(.dismiss)
2726
}
27+
28+
saveToolbarItem {
29+
await store.perform(.saveEnvironment)
30+
}
2831
}
2932
}
3033

@@ -77,18 +80,6 @@ struct SelfHostedView: View {
7780
.padding(.top, 8)
7881
}
7982

80-
/// The save button.
81-
private var saveButton: some View {
82-
AsyncButton {
83-
await store.perform(.saveEnvironment)
84-
} label: {
85-
Text(Localizations.save)
86-
}
87-
.accessibilityIdentifier("SaveButton")
88-
.buttonStyle(.primary())
89-
.padding(.top, 8)
90-
}
91-
9283
/// The self-hosted environment section.
9384
private var selfHostedEnvironment: some View {
9485
section(

BitwardenShared/UI/Auth/Landing/SelfHosted/SelfHostedViewTests.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,47 @@ import XCTest
44
@testable import BitwardenShared
55

66
class SelfHostedViewTests: BitwardenTestCase {
7-
let subject = SelfHostedView(store: Store(processor: StateProcessor(state: SelfHostedState())))
7+
// MARK: Properties
8+
9+
var processor: MockProcessor<SelfHostedState, SelfHostedAction, SelfHostedEffect>!
10+
var subject: SelfHostedView!
11+
12+
// MARK: Setup & Teardown
13+
14+
override func setUp() {
15+
super.setUp()
16+
17+
processor = MockProcessor(state: SelfHostedState())
18+
19+
subject = SelfHostedView(store: Store(processor: processor))
20+
}
21+
22+
override func tearDown() {
23+
super.tearDown()
24+
25+
subject = nil
26+
}
27+
28+
// MARK: Tests
29+
30+
/// Tapping the cancel button dispatches the `.dismiss` action.
31+
func test_cancelButton_tap() throws {
32+
let button = try subject.inspect().find(button: Localizations.cancel)
33+
try button.tap()
34+
XCTAssertEqual(processor.dispatchedActions.last, .dismiss)
35+
}
36+
37+
/// Tapping the save button dispatches the `.saveEnvironment` action.
38+
func test_saveButton_tap() async throws {
39+
let button = try subject.inspect().find(asyncButton: Localizations.save)
40+
try await button.tap()
41+
XCTAssertEqual(processor.effects.last, .saveEnvironment)
42+
}
43+
44+
// MARK: Snapshots
845

946
/// Tests that the view renders correctly.
1047
func test_viewRender() {
11-
assertSnapshot(of: subject, as: .defaultPortrait)
48+
assertSnapshot(of: subject.navStackWrapped, as: .defaultPortrait)
1249
}
1350
}

BitwardenShared/UI/Platform/Application/Extensions/View+Toolbar.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ extension View {
4848
.accessibilityIdentifier("EditItemButton")
4949
}
5050

51+
/// Returns a toolbar button configured for saving an item.
52+
///
53+
/// - Parameter action: The action to perform when the save button is tapped.
54+
/// - Returns: A `Button` configured for saving an item.
55+
///
56+
func saveToolbarButton(action: @escaping () async -> Void) -> some View {
57+
toolbarButton(Localizations.save, action: action)
58+
.accessibilityIdentifier("SaveButton")
59+
}
60+
5161
/// Returns a `Button` that displays an image for use in a toolbar.
5262
///
5363
/// - Parameters:
@@ -140,4 +150,15 @@ extension View {
140150
optionsToolbarMenu(content: content)
141151
}
142152
}
153+
154+
/// A `ToolbarItem` for views with a save button.
155+
///
156+
/// - Parameter action: The action to perform when the save button is tapped.
157+
/// - Returns: A `ToolbarItem` with a save button.
158+
///
159+
func saveToolbarItem(_ action: @escaping () async -> Void) -> some ToolbarContent {
160+
ToolbarItem(placement: .topBarTrailing) {
161+
saveToolbarButton(action: action)
162+
}
163+
}
143164
}

BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderView.swift

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ struct AddEditFolderView: View {
3131
cancelToolbarItem {
3232
store.send(.dismiss)
3333
}
34+
35+
saveToolbarItem {
36+
await store.perform(.saveTapped)
37+
}
3438
}
3539
}
3640

@@ -39,24 +43,28 @@ struct AddEditFolderView: View {
3943
content
4044
.navigationBar(title: Localizations.editFolder, titleDisplayMode: .inline)
4145
.toolbar {
42-
optionsToolbarItem {
43-
AsyncButton(Localizations.delete, role: .destructive) {
44-
await store.perform(.deleteTapped)
45-
}
46-
}
47-
4846
cancelToolbarItem {
4947
store.send(.dismiss)
5048
}
49+
50+
ToolbarItemGroup(placement: .topBarTrailing) {
51+
saveToolbarButton {
52+
await store.perform(.saveTapped)
53+
}
54+
55+
optionsToolbarMenu {
56+
AsyncButton(Localizations.delete, role: .destructive) {
57+
await store.perform(.deleteTapped)
58+
}
59+
}
60+
}
5161
}
5262
}
5363

5464
/// The content of the view in either mode.
5565
private var content: some View {
5666
VStack(alignment: .leading, spacing: 20) {
5767
nameEntryTextField
58-
59-
saveButton
6068
}
6169
.scrollView()
6270
}
@@ -71,13 +79,4 @@ struct AddEditFolderView: View {
7179
)
7280
)
7381
}
74-
75-
/// The save button.
76-
private var saveButton: some View {
77-
AsyncButton(Localizations.save) {
78-
await store.perform(.saveTapped)
79-
}
80-
.accessibilityIdentifier("SaveButton")
81-
.buttonStyle(.primary())
82-
}
8382
}

BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderViewTests.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,31 +61,48 @@ class AddEditFolderViewTests: BitwardenTestCase {
6161
XCTAssertEqual(processor.dispatchedActions.last, .folderNameTextChanged("text"))
6262
}
6363

64-
/// Tapping the save button performs the `.saveTapped` effect.
65-
func test_saveButton_tap() async throws {
64+
/// Tapping the save button in add mode performs the `.saveTapped` effect.
65+
func test_saveButton_tapAdd() async throws {
6666
let button = try subject.inspect().find(asyncButton: Localizations.save)
6767
try await button.tap()
6868

6969
XCTAssertEqual(processor.effects.last, .saveTapped)
7070
}
7171

72+
/// Tapping the save button in edit mode performs the `.saveTapped` effect.
73+
func test_saveButton_tapEdit() async throws {
74+
processor.state.mode = .edit(.fixture())
75+
let button = try subject.inspect().find(asyncButton: Localizations.save)
76+
try await button.tap()
77+
XCTAssertEqual(processor.effects.last, .saveTapped)
78+
}
79+
7280
// MARK: Snapshots
7381

7482
/// Tests the view renders correctly when the text field is empty.
7583
func test_snapshot_add_empty() {
76-
assertSnapshots(matching: subject, as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5])
84+
assertSnapshots(
85+
matching: subject.navStackWrapped,
86+
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5]
87+
)
7788
}
7889

7990
/// Tests the view renders correctly when the text field is populated.
8091
func test_snapshot_add_populated() {
8192
processor.state.folderName = "Super cool folder name"
82-
assertSnapshots(matching: subject, as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5])
93+
assertSnapshots(
94+
matching: subject.navStackWrapped,
95+
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5]
96+
)
8397
}
8498

8599
/// Tests the view renders correctly when the text field is populated.
86100
func test_snapshot_edit_populated() {
87101
processor.state.mode = .edit(.fixture())
88102
processor.state.folderName = "Super cool folder name"
89-
assertSnapshots(matching: subject, as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5])
103+
assertSnapshots(
104+
matching: subject.navStackWrapped,
105+
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5]
106+
)
90107
}
91108
}

BitwardenShared/UI/Tools/Generator/Generator/GeneratorState.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ struct GeneratorState: Equatable {
4747
}
4848
}
4949

50+
/// A flag indicating if the options toolbar button is visible.
51+
var isOptionsButtonVisible: Bool {
52+
switch self {
53+
case .tab: true
54+
case .inPlace: false
55+
}
56+
}
57+
5058
/// A flag indicating if the select button is visible.
5159
var isSelectButtonVisible: Bool {
5260
switch self {

BitwardenShared/UI/Tools/Generator/Generator/GeneratorView.swift

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,11 @@ struct GeneratorView: View {
2525
ForEach(store.state.formSections) { section in
2626
sectionView(section)
2727
}
28-
29-
if store.state.presentationMode.isSelectButtonVisible {
30-
Button(Localizations.select) {
31-
store.send(.selectButtonPressed)
32-
}
33-
.buttonStyle(.primary())
34-
.accessibilityIdentifier("SelectButton")
35-
}
3628
}
3729
.padding(16)
3830
}
3931
.background(Asset.Colors.backgroundSecondary.swiftUIColor)
40-
.navigationBarTitleDisplayMode(.large)
32+
.navigationBarTitleDisplayMode(store.state.presentationMode == .inPlace ? .inline : .large)
4133
.navigationTitle(Localizations.generator)
4234
.task { await store.perform(.appeared) }
4335
.onChange(of: focusedFieldKeyPath) { newValue in
@@ -56,10 +48,19 @@ struct GeneratorView: View {
5648
}
5749
}
5850

59-
ToolbarItem(placement: .topBarTrailing) {
60-
optionsToolbarMenu {
61-
Button(Localizations.passwordHistory) {
62-
store.send(.showPasswordHistory)
51+
ToolbarItemGroup(placement: .topBarTrailing) {
52+
if store.state.presentationMode.isSelectButtonVisible {
53+
toolbarButton(Localizations.select) {
54+
store.send(.selectButtonPressed)
55+
}
56+
.accessibilityIdentifier("SelectButton")
57+
}
58+
59+
if store.state.presentationMode.isOptionsButtonVisible {
60+
optionsToolbarMenu {
61+
Button(Localizations.passwordHistory) {
62+
store.send(.showPasswordHistory)
63+
}
6364
}
6465
}
6566
}

BitwardenShared/UI/Tools/Generator/Generator/GeneratorViewTests.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ class GeneratorViewTests: BitwardenTestCase {
9696
}
9797

9898
/// Tapping the select button dispatches the `.selectButtonPressed` action.
99-
func test_selectButton_tap() throws {
99+
func test_selectButton_tap() async throws {
100100
processor.state.presentationMode = .inPlace
101-
let button = try subject.inspect().find(button: Localizations.select)
102-
try button.tap()
101+
let button = try subject.inspect().find(asyncButton: Localizations.select)
102+
try await button.tap()
103103
XCTAssertEqual(processor.dispatchedActions.last, .selectButtonPressed)
104104
}
105105

@@ -180,7 +180,7 @@ class GeneratorViewTests: BitwardenTestCase {
180180
processor.state.generatedValue = "pa11w0rd"
181181
processor.state.showCopiedValueToast()
182182
assertSnapshot(
183-
matching: subject,
183+
matching: subject.navStackWrapped,
184184
as: .defaultPortrait
185185
)
186186
}
@@ -189,7 +189,7 @@ class GeneratorViewTests: BitwardenTestCase {
189189
func test_snapshot_generatorViewPassphrase() {
190190
processor.state.passwordState.passwordGeneratorType = .passphrase
191191
assertSnapshot(
192-
matching: subject,
192+
matching: subject.navStackWrapped,
193193
as: .defaultPortrait
194194
)
195195
}
@@ -198,7 +198,7 @@ class GeneratorViewTests: BitwardenTestCase {
198198
func test_snapshot_generatorViewPassword() {
199199
processor.state.passwordState.passwordGeneratorType = .password
200200
assertSnapshot(
201-
matching: subject,
201+
matching: subject.navStackWrapped,
202202
as: .defaultPortrait
203203
)
204204
}
@@ -207,14 +207,14 @@ class GeneratorViewTests: BitwardenTestCase {
207207
func test_snapshot_generatorViewPassword_inPlace() {
208208
processor.state.passwordState.passwordGeneratorType = .password
209209
processor.state.presentationMode = .inPlace
210-
assertSnapshot(of: subject, as: .tallPortrait)
210+
assertSnapshot(of: subject.navStackWrapped, as: .tallPortrait)
211211
}
212212

213213
/// Test a snapshot of the password generation view with a policy in effect.
214214
func test_snapshot_generatorViewPassword_policyInEffect() {
215215
processor.state.isPolicyInEffect = true
216216
assertSnapshot(
217-
matching: subject,
217+
matching: subject.navStackWrapped,
218218
as: .defaultPortrait
219219
)
220220
}
@@ -224,7 +224,7 @@ class GeneratorViewTests: BitwardenTestCase {
224224
processor.state.generatorType = .username
225225
processor.state.usernameState.usernameGeneratorType = .catchAllEmail
226226
assertSnapshot(
227-
matching: subject,
227+
matching: subject.navStackWrapped,
228228
as: .defaultPortrait
229229
)
230230
}
@@ -234,7 +234,7 @@ class GeneratorViewTests: BitwardenTestCase {
234234
processor.state.generatorType = .username
235235
processor.state.usernameState.usernameGeneratorType = .forwardedEmail
236236
assertSnapshot(
237-
matching: subject,
237+
matching: subject.navStackWrapped,
238238
as: .defaultPortrait
239239
)
240240
}
@@ -244,7 +244,7 @@ class GeneratorViewTests: BitwardenTestCase {
244244
processor.state.generatorType = .username
245245
processor.state.usernameState.usernameGeneratorType = .plusAddressedEmail
246246
assertSnapshot(
247-
matching: subject,
247+
matching: subject.navStackWrapped,
248248
as: .defaultPortrait
249249
)
250250
}
@@ -254,15 +254,15 @@ class GeneratorViewTests: BitwardenTestCase {
254254
processor.state.generatorType = .username
255255
processor.state.usernameState.usernameGeneratorType = .plusAddressedEmail
256256
processor.state.presentationMode = .inPlace
257-
assertSnapshot(matching: subject, as: .defaultPortrait)
257+
assertSnapshot(matching: subject.navStackWrapped, as: .defaultPortrait)
258258
}
259259

260260
/// Test a snapshot of the random word username generation view.
261261
func test_snapshot_generatorViewUsernameRandomWord() {
262262
processor.state.generatorType = .username
263263
processor.state.usernameState.usernameGeneratorType = .randomWord
264264
assertSnapshot(
265-
matching: subject,
265+
matching: subject.navStackWrapped,
266266
as: .defaultPortrait
267267
)
268268
}

0 commit comments

Comments
 (0)