Skip to content

Commit a494aeb

Browse files
feat: add hidden files toggle to remote file picker (#269)
1 parent dc57c40 commit a494aeb

2 files changed

Lines changed: 74 additions & 9 deletions

File tree

‎Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ struct FilePicker: View {
66
@Environment(\.dismiss) var dismiss
77
@StateObject private var model: FilePickerModel
88
@State private var selection: FilePickerEntryModel?
9+
@AppStorage("ShowHiddenFilesInRemoteFilePicker") private var showHiddenFiles = false
910

1011
@Binding var outputAbsPath: String
1112

@@ -34,8 +35,8 @@ struct FilePicker: View {
3435
.padding()
3536
} else {
3637
List(selection: $selection) {
37-
ForEach(model.rootEntries) { entry in
38-
FilePickerEntry(entry: entry).tag(entry)
38+
ForEach(model.rootEntries.filter { showHiddenFiles || !$0.isHidden }) { entry in
39+
FilePickerEntry(entry: entry, showHiddenFiles: showHiddenFiles).tag(entry)
3940
}
4041
}.contextMenu(
4142
forSelectionType: FilePickerEntryModel.self,
@@ -49,6 +50,9 @@ struct FilePicker: View {
4950
}
5051
Divider()
5152
HStack {
53+
Toggle(isOn: $showHiddenFiles) {
54+
Text("Show hidden files")
55+
}
5256
Spacer()
5357
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
5458
Button("Select", action: submit).keyboardShortcut(.defaultAction).disabled(selection == nil)
@@ -57,6 +61,11 @@ struct FilePicker: View {
5761
.onAppear {
5862
model.loadRoot()
5963
}
64+
.onChange(of: showHiddenFiles) {
65+
if !showHiddenFiles, selection?.isHidden == true {
66+
selection = nil
67+
}
68+
}
6069
.onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
6170
}
6271

@@ -99,6 +108,7 @@ class FilePickerModel: ObservableObject {
99108

100109
struct FilePickerEntry: View {
101110
@ObservedObject var entry: FilePickerEntryModel
111+
let showHiddenFiles: Bool
102112

103113
var body: some View {
104114
Group {
@@ -116,8 +126,8 @@ struct FilePickerEntry: View {
116126
private var directory: some View {
117127
DisclosureGroup(isExpanded: $entry.isExpanded) {
118128
if let entries = entry.entries {
119-
ForEach(entries) { entry in
120-
FilePickerEntry(entry: entry).tag(entry)
129+
ForEach(entries.filter { showHiddenFiles || !$0.isHidden }) { entry in
130+
FilePickerEntry(entry: entry, showHiddenFiles: showHiddenFiles).tag(entry)
121131
}
122132
}
123133
} label: {
@@ -150,6 +160,7 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
150160
let path: [String]
151161
let absolute_path: String
152162
let dir: Bool
163+
var isHidden: Bool { path.contains { $0.hasPrefix(".") } }
153164

154165
let client: AgentClient
155166

@@ -221,11 +232,8 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
221232
extension LSResponse {
222233
@MainActor
223234
func toModels(client: AgentClient) -> [FilePickerEntryModel] {
224-
contents.compactMap { entry in
225-
// Filter dotfiles from the picker
226-
guard !entry.name.hasPrefix(".") else { return nil }
227-
228-
return FilePickerEntryModel(
235+
contents.map { entry in
236+
FilePickerEntryModel(
229237
name: entry.name,
230238
client: client,
231239
absolute_path: entry.absolute_path_string,

‎Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,63 @@ struct FilePickerTests {
7777
}
7878
}
7979

80+
@Test
81+
func testHiddenFilesToggle() async throws {
82+
let key = "ShowHiddenFilesInRemoteFilePicker"
83+
UserDefaults.standard.removeObject(forKey: key)
84+
defer { UserDefaults.standard.removeObject(forKey: key) }
85+
86+
let hiddenMockResponse = LSResponse(
87+
absolute_path: ["/"],
88+
absolute_path_string: "/",
89+
contents: [
90+
LSFile(name: ".mux", absolute_path_string: "/.mux", is_dir: true),
91+
LSFile(name: "home", absolute_path_string: "/home", is_dir: true),
92+
LSFile(name: "README.md", absolute_path_string: "/README.md", is_dir: false),
93+
]
94+
)
95+
let host = "test-hidden-toggle.coder"
96+
let sut = FilePicker(host: host, outputAbsPath: .constant(""))
97+
let view = sut
98+
99+
let url = URL(string: "http://\(host):4")!
100+
101+
try Mock(
102+
url: url.appendingPathComponent("/api/v0/list-directory"),
103+
statusCode: 200,
104+
data: [.post: CoderSDK.encoder.encode(hiddenMockResponse)]
105+
).register()
106+
107+
try await ViewHosting.host(view) {
108+
try await sut.inspection.inspect { view in
109+
try #expect(await eventually { @MainActor in
110+
_ = try view.find(ViewType.List.self)
111+
return true
112+
})
113+
_ = try view.find(text: "README.md")
114+
var toggle = try view.find(ViewType.Toggle.self)
115+
#expect(try toggle.labelView().text().string() == "Show hidden files")
116+
#expect(try !toggle.isOn())
117+
#expect(throws: (any Error).self) { _ = try view.find(text: ".mux") }
118+
#expect(view.findAll(ViewType.DisclosureGroup.self).count == 1)
119+
120+
try toggle.tap()
121+
toggle = try view.find(ViewType.Toggle.self)
122+
#expect(try toggle.isOn())
123+
#expect(UserDefaults.standard.bool(forKey: key))
124+
_ = try view.find(text: ".mux")
125+
#expect(view.findAll(ViewType.DisclosureGroup.self).count == 2)
126+
127+
try toggle.tap()
128+
toggle = try view.find(ViewType.Toggle.self)
129+
#expect(try !toggle.isOn())
130+
#expect(!UserDefaults.standard.bool(forKey: key))
131+
#expect(throws: (any Error).self) { _ = try view.find(text: ".mux") }
132+
#expect(view.findAll(ViewType.DisclosureGroup.self).count == 1)
133+
}
134+
}
135+
}
136+
80137
@Test
81138
func testDirectoryExpansion() async throws {
82139
let host = "test-expansion.coder"

0 commit comments

Comments
 (0)