Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct FilePicker: View {
@Environment(\.dismiss) var dismiss
@StateObject private var model: FilePickerModel
@State private var selection: FilePickerEntryModel?
@AppStorage("ShowHiddenFilesInRemoteFilePicker") private var showHiddenFiles = false

@Binding var outputAbsPath: String

Expand Down Expand Up @@ -34,8 +35,8 @@ struct FilePicker: View {
.padding()
} else {
List(selection: $selection) {
ForEach(model.rootEntries) { entry in
FilePickerEntry(entry: entry).tag(entry)
ForEach(model.rootEntries.filter { showHiddenFiles || !$0.isHidden }) { entry in
FilePickerEntry(entry: entry, showHiddenFiles: showHiddenFiles).tag(entry)
}
}.contextMenu(
forSelectionType: FilePickerEntryModel.self,
Expand All @@ -49,6 +50,9 @@ struct FilePicker: View {
}
Divider()
HStack {
Toggle(isOn: $showHiddenFiles) {
Text("Show hidden files")
}
Spacer()
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
Button("Select", action: submit).keyboardShortcut(.defaultAction).disabled(selection == nil)
Expand All @@ -57,6 +61,11 @@ struct FilePicker: View {
.onAppear {
model.loadRoot()
}
.onChange(of: showHiddenFiles) {
if !showHiddenFiles, selection?.isHidden == true {
selection = nil
}
}
.onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
}

Expand Down Expand Up @@ -99,6 +108,7 @@ class FilePickerModel: ObservableObject {

struct FilePickerEntry: View {
@ObservedObject var entry: FilePickerEntryModel
let showHiddenFiles: Bool

var body: some View {
Group {
Expand All @@ -116,8 +126,8 @@ struct FilePickerEntry: View {
private var directory: some View {
DisclosureGroup(isExpanded: $entry.isExpanded) {
if let entries = entry.entries {
ForEach(entries) { entry in
FilePickerEntry(entry: entry).tag(entry)
ForEach(entries.filter { showHiddenFiles || !$0.isHidden }) { entry in
FilePickerEntry(entry: entry, showHiddenFiles: showHiddenFiles).tag(entry)
}
}
} label: {
Expand Down Expand Up @@ -150,6 +160,7 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
let path: [String]
let absolute_path: String
let dir: Bool
var isHidden: Bool { path.contains { $0.hasPrefix(".") } }

let client: AgentClient

Expand Down Expand Up @@ -221,11 +232,8 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
extension LSResponse {
@MainActor
func toModels(client: AgentClient) -> [FilePickerEntryModel] {
contents.compactMap { entry in
// Filter dotfiles from the picker
guard !entry.name.hasPrefix(".") else { return nil }

return FilePickerEntryModel(
contents.map { entry in
FilePickerEntryModel(
name: entry.name,
client: client,
absolute_path: entry.absolute_path_string,
Expand Down
57 changes: 57 additions & 0 deletions Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,63 @@ struct FilePickerTests {
}
}

@Test
func testHiddenFilesToggle() async throws {
let key = "ShowHiddenFilesInRemoteFilePicker"
UserDefaults.standard.removeObject(forKey: key)
defer { UserDefaults.standard.removeObject(forKey: key) }

let hiddenMockResponse = LSResponse(
absolute_path: ["/"],
absolute_path_string: "/",
contents: [
LSFile(name: ".mux", absolute_path_string: "/.mux", is_dir: true),
LSFile(name: "home", absolute_path_string: "/home", is_dir: true),
LSFile(name: "README.md", absolute_path_string: "/README.md", is_dir: false),
]
)
let host = "test-hidden-toggle.coder"
let sut = FilePicker(host: host, outputAbsPath: .constant(""))
let view = sut

let url = URL(string: "http://\(host):4")!

try Mock(
url: url.appendingPathComponent("/api/v0/list-directory"),
statusCode: 200,
data: [.post: CoderSDK.encoder.encode(hiddenMockResponse)]
).register()

try await ViewHosting.host(view) {
try await sut.inspection.inspect { view in
try #expect(await eventually { @MainActor in
_ = try view.find(ViewType.List.self)
return true
})
_ = try view.find(text: "README.md")
var toggle = try view.find(ViewType.Toggle.self)
#expect(try toggle.labelView().text().string() == "Show hidden files")
#expect(try !toggle.isOn())
#expect(throws: (any Error).self) { _ = try view.find(text: ".mux") }
#expect(view.findAll(ViewType.DisclosureGroup.self).count == 1)

try toggle.tap()
toggle = try view.find(ViewType.Toggle.self)
#expect(try toggle.isOn())
#expect(UserDefaults.standard.bool(forKey: key))
_ = try view.find(text: ".mux")
#expect(view.findAll(ViewType.DisclosureGroup.self).count == 2)

try toggle.tap()
toggle = try view.find(ViewType.Toggle.self)
#expect(try !toggle.isOn())
#expect(!UserDefaults.standard.bool(forKey: key))
#expect(throws: (any Error).self) { _ = try view.find(text: ".mux") }
#expect(view.findAll(ViewType.DisclosureGroup.self).count == 1)
}
}
}

@Test
func testDirectoryExpansion() async throws {
let host = "test-expansion.coder"
Expand Down
Loading