-
-
Notifications
You must be signed in to change notification settings - Fork 291
feat(datagrid): redesign filter panel header and row actions #1649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa8a795
3cd715a
e76906c
4daced2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // | ||
| // TristateCheckbox.swift | ||
| // TablePro | ||
| // | ||
|
|
||
| import AppKit | ||
| import SwiftUI | ||
|
|
||
| struct TristateCheckbox: NSViewRepresentable { | ||
| enum State { | ||
| case unchecked, checked, mixed | ||
|
|
||
| init(allEnabled: Bool?) { | ||
| switch allEnabled { | ||
| case .some(true): self = .checked | ||
| case .some(false): self = .unchecked | ||
| case .none: self = .mixed | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let state: State | ||
| let action: () -> Void | ||
|
|
||
| func makeNSView(context: Context) -> NSButton { | ||
| let button = NSButton(checkboxWithTitle: "", target: context.coordinator, action: #selector(Coordinator.clicked)) | ||
| button.allowsMixedState = true | ||
| button.setContentHuggingPriority(.defaultHigh, for: .horizontal) | ||
| button.setContentHuggingPriority(.defaultHigh, for: .vertical) | ||
| return button | ||
| } | ||
|
|
||
| func updateNSView(_ button: NSButton, context: Context) { | ||
| switch state { | ||
| case .unchecked: button.state = .off | ||
| case .checked: button.state = .on | ||
| case .mixed: button.state = .mixed | ||
| } | ||
| context.coordinator.action = action | ||
| } | ||
|
|
||
| func makeCoordinator() -> Coordinator { | ||
| Coordinator(action: action) | ||
| } | ||
|
|
||
| class Coordinator: NSObject { | ||
| var action: () -> Void | ||
|
|
||
| init(action: @escaping () -> Void) { | ||
| self.action = action | ||
| } | ||
|
|
||
| @objc func clicked() { | ||
| action() | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,8 +67,26 @@ struct FilterPanelView: View { | |
| .onPreferenceChange(FilterRowsHeightKey.self) { filterRowsHeight = $0 } | ||
| } | ||
|
|
||
| private func toggleAllFiltersEnabled() { | ||
| let newState = filterState.allEnabledState != true | ||
| for filter in filterState.filters { | ||
| var updated = filter | ||
| updated.isEnabled = newState | ||
| coordinator.updateFilter(updated) | ||
| } | ||
| } | ||
|
|
||
| private var filterHeader: some View { | ||
| HStack(spacing: 8) { | ||
| if !filterState.filters.isEmpty { | ||
| TristateCheckbox( | ||
| state: TristateCheckbox.State(allEnabled: filterState.allEnabledState), | ||
| action: toggleAllFiltersEnabled | ||
| ) | ||
| .help(String(localized: "Enable or disable all filters")) | ||
| .accessibilityLabel(String(localized: "Enable or disable all filters")) | ||
| } | ||
|
|
||
| Text("Filters") | ||
| .font(.callout.weight(.medium)) | ||
|
|
||
|
|
@@ -88,15 +106,15 @@ struct FilterPanelView: View { | |
|
|
||
| filterOptionsMenu | ||
|
|
||
| Button("Unset") { | ||
| coordinator.clearFilterState() | ||
| Button("Clear") { | ||
| coordinator.clearAppliedFilters() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When there are unsaved row edits, Useful? React with 👍 / 👎. |
||
| onUnset() | ||
| coordinator.focusActiveGrid() | ||
| } | ||
| .buttonStyle(.bordered) | ||
| .controlSize(.small) | ||
| .disabled(!filterState.hasAppliedFilters) | ||
| .help(String(localized: "Remove all filters and reload")) | ||
| .help(String(localized: "Clear applied filters without removing filter rows")) | ||
|
|
||
| Button("Apply") { | ||
| applyAllValidFilters() | ||
|
|
@@ -172,6 +190,17 @@ struct FilterPanelView: View { | |
|
|
||
| Divider() | ||
|
|
||
| Button(role: .destructive) { | ||
| coordinator.clearFilterState() | ||
| onUnset() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the menu is used while the panel only has draft rows (for example the auto-added blank row before anything has been applied), this still invokes Useful? React with 👍 / 👎. |
||
| coordinator.focusActiveGrid() | ||
| } label: { | ||
| Label(String(localized: "Remove All Filters"), systemImage: "xmark.circle") | ||
| } | ||
| .disabled(filterState.filters.isEmpty) | ||
|
|
||
| Divider() | ||
|
|
||
| Button { | ||
| showSettingsPopover.toggle() | ||
| } label: { | ||
|
|
@@ -198,7 +227,6 @@ struct FilterPanelView: View { | |
| completions: completionItems(), | ||
| enumValuesByColumn: enumValuesByColumn, | ||
| rawSQLCompletionProvider: rawSQLCompletionProvider, | ||
| isApplied: filterState.commit == .solo(filter.id), | ||
| onAdd: { | ||
| coordinator.addFilter(columns: columns, primaryKeyColumn: primaryKeyColumn) | ||
| focusedFilterId = filterState.filters.last?.id | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the table is filtered via an
.allcommit and every applied row is enabled, clicking the new header checkbox reaches this path and flips every row toisEnabled = falsewithout reloading. BecausehasAppliedFiltersis derived from the now-disabled rows, the panel immediately reports no applied filters and disables both Clear and Apply, while the grid is still showing the old filtered query; users then have to remove the rows or re-enable a filter to get back to an unfiltered table. Keep the applied-state controls in sync with the actual query, or allow this toggle to trigger/enable the clear reload path.Useful? React with 👍 / 👎.