From 057c9d0845cb335f8b8b5486bfe5a85a53e2ed8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o=20Gon=C3=A7alves?= <5808343+bgoncal@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:10:17 +0100 Subject: [PATCH] Add servers as filter to pick entities in the EntityPicker flow (#3444) --- HomeAssistant.xcodeproj/project.pbxproj | 8 ++ .../ClientEventsLogView.swift | 19 ++--- .../MagicItem/Add/MagicItemAddView.swift | 12 ++- .../MagicItem/Add/MagicItemAddViewModel.swift | 1 + .../App/Settings/MagicItem/EntityPicker.swift | 83 +++++++++++-------- .../DesignSystem/Components/PillView.swift | 28 +++++++ .../Components/ServersPickerPillList.swift | 47 +++++++++++ 7 files changed, 150 insertions(+), 48 deletions(-) create mode 100644 Sources/Shared/DesignSystem/Components/PillView.swift create mode 100644 Sources/Shared/DesignSystem/Components/ServersPickerPillList.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index e124ad389..0c8140372 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -798,6 +798,7 @@ 42C1012B2CD3DB8A0012BA78 /* CoverIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C101292CD3DB8A0012BA78 /* CoverIntent.swift */; }; 42C1012E2CD3DBF00012BA78 /* ControlCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C1012C2CD3DBF00012BA78 /* ControlCover.swift */; }; 42C101302CD3DC0C0012BA78 /* ControlCoverValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C1012F2CD3DC0C0012BA78 /* ControlCoverValueProvider.swift */; }; + 42C131D02D66084C00AF48E6 /* PillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C131CF2D66084C00AF48E6 /* PillView.swift */; }; 42C3737F2BC415AC00898990 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C3737E2BC415AC00898990 /* UIViewController+Extensions.swift */; }; 42C373B22BC5382900898990 /* HostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C373B12BC5382900898990 /* HostingController.swift */; }; 42CE8FA72B45D1E900C707F9 /* CoreStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CE8FA52B45D1E900C707F9 /* CoreStrings.swift */; }; @@ -856,6 +857,7 @@ 42EFFAEC2C8882DD002F10FC /* CarPlayConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EFFAEB2C8882DD002F10FC /* CarPlayConfigurationView.swift */; }; 42F158462CA15C99009C7201 /* ControlSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F158452CA15C99009C7201 /* ControlSwitch.swift */; }; 42F158482CA15FA7009C7201 /* SwitchIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F158472CA15FA7009C7201 /* SwitchIntent.swift */; }; + 42F161442D661D11003DDC75 /* ServersPickerPillList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F161422D661CBA003DDC75 /* ServersPickerPillList.swift */; }; 42F1DA5B2B4BF7DF002729BC /* WindowSizeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F1DA5A2B4BF7DF002729BC /* WindowSizeObserver.swift */; }; 42F1DA5D2B4BF85F002729BC /* WindowScenesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F1DA5C2B4BF85F002729BC /* WindowScenesManager.swift */; }; 42F1DA5F2B4D4B32002729BC /* CarPlayServerListTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F1DA5E2B4D4B32002729BC /* CarPlayServerListTemplate.swift */; }; @@ -2137,6 +2139,7 @@ 42C101292CD3DB8A0012BA78 /* CoverIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverIntent.swift; sourceTree = ""; }; 42C1012C2CD3DBF00012BA78 /* ControlCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlCover.swift; sourceTree = ""; }; 42C1012F2CD3DC0C0012BA78 /* ControlCoverValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlCoverValueProvider.swift; sourceTree = ""; }; + 42C131CF2D66084C00AF48E6 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = ""; }; 42C3737E2BC415AC00898990 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; 42C373AF2BC536AA00898990 /* WatchApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WatchApp-Bridging-Header.h"; sourceTree = ""; }; 42C373B12BC5382900898990 /* HostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingController.swift; sourceTree = ""; }; @@ -2188,6 +2191,7 @@ 42EFFAEB2C8882DD002F10FC /* CarPlayConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayConfigurationView.swift; sourceTree = ""; }; 42F158452CA15C99009C7201 /* ControlSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlSwitch.swift; sourceTree = ""; }; 42F158472CA15FA7009C7201 /* SwitchIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchIntent.swift; sourceTree = ""; }; + 42F161422D661CBA003DDC75 /* ServersPickerPillList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersPickerPillList.swift; sourceTree = ""; }; 42F1DA5A2B4BF7DF002729BC /* WindowSizeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowSizeObserver.swift; sourceTree = ""; }; 42F1DA5C2B4BF85F002729BC /* WindowScenesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowScenesManager.swift; sourceTree = ""; }; 42F1DA5E2B4D4B32002729BC /* CarPlayServerListTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayServerListTemplate.swift; sourceTree = ""; }; @@ -4298,6 +4302,7 @@ 42CA28B12B101D9C0093B31A /* Components */ = { isa = PBXGroup; children = ( + 42F161422D661CBA003DDC75 /* ServersPickerPillList.swift */, 429BEA1B2D1030EA00F070F9 /* SheetCloseButton.swift */, 42FCD0052B9B1D9E0057783F /* CollapsibleView.swift */, 42CA28AF2B101D6B0093B31A /* CardView.swift */, @@ -4305,6 +4310,7 @@ 42790C452C4808FA00E31B38 /* AppleLikeBottomSheet.swift */, 4254C4CC2D103F7B00245021 /* ExternalLinkButton.swift */, 42B74A5C2D36A47E00C37304 /* CloseButton.swift */, + 42C131CF2D66084C00AF48E6 /* PillView.swift */, ); path = Components; sourceTree = ""; @@ -7571,6 +7577,7 @@ 118F046924CB895A00CBBD5C /* UIColor+CSS3+Hex.swift in Sources */, 1109F81F24A1C011002590F2 /* SensorProvider.swift in Sources */, 4254C4CA2D103ABB00245021 /* ExternalLink.swift in Sources */, + 42F161442D661D11003DDC75 /* ServersPickerPillList.swift in Sources */, 4297ADA82C89C74A00790812 /* GRDB+Initialization.swift in Sources */, 1141182A24AFA10900E6525C /* WebhookResponseHandler.swift in Sources */, 420CFC792D3F9CAB009A94F3 /* AppEntityRegistryListForDisplayTable.swift in Sources */, @@ -7761,6 +7768,7 @@ D05A4D32216DD206009FD1EB /* MJPEGStreamer.swift in Sources */, 426266452C11B02C0081A818 /* InteractiveImmediateMessages.swift in Sources */, 42CE8FB02B46C3D900C707F9 /* CoreStrings+Values.swift in Sources */, + 42C131D02D66084C00AF48E6 /* PillView.swift in Sources */, 11C4628B24B1230E00031902 /* WebhookResponseServiceCall.swift in Sources */, D0EEF30A214DD64C00D1D360 /* UIImage+Icons.swift in Sources */, 42B94BED2B96083C00DEE060 /* AssistModel.swift in Sources */, diff --git a/Sources/App/Settings/ClientEventsLogView/ClientEventsLogView.swift b/Sources/App/Settings/ClientEventsLogView/ClientEventsLogView.swift index a4b2325bd..4ffd25e39 100644 --- a/Sources/App/Settings/ClientEventsLogView/ClientEventsLogView.swift +++ b/Sources/App/Settings/ClientEventsLogView/ClientEventsLogView.swift @@ -90,7 +90,10 @@ struct ClientEventsLogView: View { Button { viewModel.resetTypeFilter() } label: { - filterPill(L10n.ClientEvents.EventType.all, selected: viewModel.typeFilter == nil) + PillView( + text: L10n.ClientEvents.EventType.all, + selected: viewModel.typeFilter == nil + ) } ForEach(ClientEvent.EventType.allCases.sorted { e1, e2 in e1.displayText < e2.displayText @@ -98,7 +101,10 @@ struct ClientEventsLogView: View { Button { viewModel.typeFilter = type } label: { - filterPill(type.displayText, selected: viewModel.typeFilter == type) + PillView( + text: type.displayText, + selected: viewModel.typeFilter == type + ) } } } @@ -124,15 +130,6 @@ struct ClientEventsLogView: View { } } - private func filterPill(_ text: String, selected: Bool) -> some View { - Text(text) - .foregroundStyle(selected ? .white : Color(uiColor: .label)) - .padding(Spaces.one) - .padding(.horizontal) - .background(selected ? Color.asset(Asset.Colors.haPrimary) : Color.secondary.opacity(0.1)) - .clipShape(Capsule()) - } - private func listItem(_ event: ClientEvent) -> some View { NavigationLink { eventDescription(event) diff --git a/Sources/App/Settings/MagicItem/Add/MagicItemAddView.swift b/Sources/App/Settings/MagicItem/Add/MagicItemAddView.swift index 87bafc99f..79f01b3ce 100644 --- a/Sources/App/Settings/MagicItem/Add/MagicItemAddView.swift +++ b/Sources/App/Settings/MagicItem/Add/MagicItemAddView.swift @@ -65,6 +65,10 @@ struct MagicItemAddView: View { .onAppear { autoSelectItemType() viewModel.loadContent() + + if viewModel.selectedServerId == nil { + viewModel.selectedServerId = Current.servers.all.first?.identifier.rawValue + } } .toolbar(content: { CloseButton { @@ -159,10 +163,10 @@ struct MagicItemAddView: View { @ViewBuilder private var entitiesPerServerList: some View { - ForEach(Array(viewModel.entities.keys), id: \.identifier) { server in - Section(server.info.name) { - list(entities: viewModel.entities[server] ?? [], serverId: server.identifier.rawValue, type: .entity) - } + ServersPickerPillList(selectedServerId: $viewModel.selectedServerId) + if let server = Current.servers.all + .first(where: { $0.identifier.rawValue == viewModel.selectedServerId }) ?? Current.servers.all.first { + list(entities: viewModel.entities[server] ?? [], serverId: server.identifier.rawValue, type: .entity) } } diff --git a/Sources/App/Settings/MagicItem/Add/MagicItemAddViewModel.swift b/Sources/App/Settings/MagicItem/Add/MagicItemAddViewModel.swift index e10ff1f5a..285b6174b 100644 --- a/Sources/App/Settings/MagicItem/Add/MagicItemAddViewModel.swift +++ b/Sources/App/Settings/MagicItem/Add/MagicItemAddViewModel.swift @@ -18,6 +18,7 @@ final class MagicItemAddViewModel: ObservableObject { @Published var entities: [Server: [HAAppEntity]] = [:] @Published var actions: [Action] = [] @Published var searchText: String = "" + @Published var selectedServerId: String? private var entitiesSubscription: AnyCancellable? diff --git a/Sources/App/Settings/MagicItem/EntityPicker.swift b/Sources/App/Settings/MagicItem/EntityPicker.swift index 144dd9112..ddb60911d 100644 --- a/Sources/App/Settings/MagicItem/EntityPicker.swift +++ b/Sources/App/Settings/MagicItem/EntityPicker.swift @@ -9,6 +9,7 @@ struct EntityPicker: View { @State private var entities: [HAAppEntity] = [] @Binding private var selectedEntity: HAAppEntity? @State private var searchTerm = "" + @State private var selectedServerId: String? init(selectedEntity: Binding, domainFilter: Domain?) { self.domainFilter = domainFilter @@ -16,6 +17,13 @@ struct EntityPicker: View { } var body: some View { + button + .sheet(isPresented: $showList) { + screen + } + } + + private var button: some View { Button(action: { showList = true }, label: { @@ -25,45 +33,54 @@ struct EntityPicker: View { Text(L10n.EntityPicker.placeholder) } }) - .sheet(isPresented: $showList) { - NavigationView { - List { - ForEach(Current.servers.all, id: \.identifier) { server in - Section(server.info.name) { - ForEach(entities.filter({ entity in - if searchTerm.count > 2 { - return entity.serverId == server.identifier.rawValue && ( - entity.name.lowercased().contains(searchTerm.lowercased()) || - entity.entityId.lowercased().contains(searchTerm.lowercased()) - ) + } + + private var screen: some View { + NavigationView { + List { + ServersPickerPillList(selectedServerId: $selectedServerId) + ForEach(entities.filter({ entity in + if searchTerm.count > 2 { + return entity.serverId == selectedServerId && ( + entity.name.lowercased().contains(searchTerm.lowercased()) || + entity.entityId.lowercased().contains(searchTerm.lowercased()) + ) + } else { + return entity.serverId == selectedServerId + } + }), id: \.id) { entity in + Button(action: { + selectedEntity = entity + showList = false + }, label: { + VStack { + Group { + if let selectedEntity, selectedEntity == entity { + Label(entity.name, systemSymbol: .checkmark) } else { - return entity.serverId == server.identifier.rawValue + Text(entity.name) } - }), id: \.id) { entity in - Button(action: { - selectedEntity = entity - showList = false - }, label: { - if let selectedEntity, selectedEntity == entity { - Label(entity.name, systemSymbol: .checkmark) - } else { - Text(entity.name) - } - }) - .tint(.accentColor) + Text(entity.entityId) + .font(.footnote) + .foregroundStyle(.secondary) } + .frame(maxWidth: .infinity, alignment: .leading) } - } + }) + .tint(.accentColor) } - .searchable(text: $searchTerm) - .onAppear { - fetchEntities() + } + .searchable(text: $searchTerm) + .onAppear { + fetchEntities() + if selectedServerId == nil { + selectedServerId = Current.servers.all.first?.identifier.rawValue } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - CloseButton { - showList = false - } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + CloseButton { + showList = false } } } diff --git a/Sources/Shared/DesignSystem/Components/PillView.swift b/Sources/Shared/DesignSystem/Components/PillView.swift new file mode 100644 index 000000000..38824c899 --- /dev/null +++ b/Sources/Shared/DesignSystem/Components/PillView.swift @@ -0,0 +1,28 @@ +import SwiftUI + +public struct PillView: View { + private let selected: Bool + private let text: String + + public init(text: String, selected: Bool) { + self.text = text + self.selected = selected + } + + public var body: some View { + Text(text) + .foregroundStyle(selected ? .white : Color(uiColor: .label)) + .padding(Spaces.one) + .padding(.horizontal) + .background(selected ? Color.asset(Asset.Colors.haPrimary) : Color.secondary.opacity(0.1)) + .clipShape(Capsule()) + } +} + +#Preview { + HStack { + PillView(text: "Value1", selected: true) + PillView(text: "Value2", selected: false) + PillView(text: "Value3", selected: false) + } +} diff --git a/Sources/Shared/DesignSystem/Components/ServersPickerPillList.swift b/Sources/Shared/DesignSystem/Components/ServersPickerPillList.swift new file mode 100644 index 000000000..b5879fdc3 --- /dev/null +++ b/Sources/Shared/DesignSystem/Components/ServersPickerPillList.swift @@ -0,0 +1,47 @@ +import SwiftUI + +public struct ServersPickerPillList: View { + @Binding private var selectedServerId: String? + + public init(selectedServerId: Binding) { + self._selectedServerId = selectedServerId + } + + public var body: some View { + serversList + } + + @ViewBuilder + private var serversList: some View { + if Current.servers.all.count > 1 { + Section { + ScrollView(.horizontal) { + HStack { + ForEach(Current.servers.all.sorted(by: { lhs, rhs in + lhs.info.sortOrder < rhs.info.sortOrder + }), id: \.identifier) { server in + Button { + selectedServerId = server.identifier.rawValue + } label: { + PillView( + text: server.info.name, + selected: selectedServerId == server.identifier.rawValue + ) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .listRowBackground(Color.clear) + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + } + } +} + +#Preview { + List { + ServersPickerPillList(selectedServerId: .constant("1")) + } +}