Skip to content
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

feat: 🎸 sort & filter enhancement part 2 #820

Merged
merged 9 commits into from
Oct 17, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ struct SortFilterExample: View {
[
.switch(item: .init(name: "Favorite", value: true, icon: "heart.fill"), showsOnFilterFeedbackBar: true),
.switch(item: .init(name: "Tagged", value: nil, icon: "tag"), showsOnFilterFeedbackBar: false),
.picker(item: .init(name: "JIRA Status", value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review Pending Pending Pending Pending Pending", "Accepted Medium", "Rejected"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "clock", itemLayout: .flexible), showsOnFilterFeedbackBar: true)
.picker(item: .init(name: "JIRA Status", value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review Pending Pending Pending Pending Pending", "Accepted Medium", "Pending Medium", "Completed Medium"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "clock", itemLayout: .flexible, displayMode: .automatic), showsOnFilterFeedbackBar: true)
],
[
.picker(item: .init(name: "Priority", value: [0], valueOptions: ["High", "Medium", "Low"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "filemenu.and.cursorarrow"), showsOnFilterFeedbackBar: true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ extension View {
return self.json(item: v)
case .switch(let v, _):
return self.json(item: v)
case .listPicker(let v, _):
return self.json(item: v)
}
}

Expand All @@ -32,4 +34,8 @@ extension View {
func json(item: SortFilterItem.SwitchItem) -> String {
"{name: \(item.name), value: \(String(describing: item.value))}"
}

func json(item: SortFilterItem.ListPickerItem) -> String {
"{name: \(item.name), value: \(item.value)}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,53 @@ struct CancellableResettableDialogForm<Title: View, CancelAction: View, ResetAct
}
}

struct CancellableResettableDialogNavigationForm<Title: View, CancelAction: View, ResetAction: View, ApplyAction: View, Components: View>: View {
let title: Title

let components: Components

var cancelAction: CancelAction
var resetAction: ResetAction
var applyAction: ApplyAction

public init(@ViewBuilder title: () -> Title,
@ViewBuilder cancelAction: () -> CancelAction,
@ViewBuilder resetAction: () -> ResetAction,
@ViewBuilder applyAction: () -> ApplyAction,
@ViewBuilder components: () -> Components)
{
self.title = title()
self.cancelAction = cancelAction()
self.resetAction = resetAction()
self.applyAction = applyAction()
self.components = components()
}

var body: some View {
NavigationStack {
VStack(spacing: UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) {
self.components.background(Color.preferredColor(.secondaryGroupedBackground))
self.applyAction
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
self.title
}
ToolbarItem(placement: .topBarLeading) {
self.cancelAction
}
ToolbarItem(placement: .topBarTrailing) {
self.resetAction
}
}
}
.frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 375 : Screen.bounds.size.width)
.padding([.bottom], UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16)
.background(Color.preferredColor(.chromeSecondary))
}
}

struct ApplyButtonStyle: PrimitiveButtonStyle {
@Environment(\.isEnabled) private var isEnabled: Bool

Expand Down
195 changes: 193 additions & 2 deletions Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public enum SortFilterItem: Identifiable, Hashable {
return item.id
case .datetime(let item, _):
return item.id
case .listPicker(let item, _):
return item.id
}
}

Expand Down Expand Up @@ -58,6 +60,8 @@ public enum SortFilterItem: Identifiable, Hashable {
/// 2. A section of view containing a SwiftUI Canlendar
case datetime(item: DateTimeItem, showsOnFilterFeedbackBar: Bool)

case listPicker(item: ListPickerItem, showsOnFilterFeedbackBar: Bool)

public var showsOnFilterFeedbackBar: Bool {
switch self {
case .picker(_, let showsOnFilterFeedbackBar):
Expand All @@ -70,6 +74,8 @@ public enum SortFilterItem: Identifiable, Hashable {
return showsOnFilterFeedbackBar
case .datetime(_, let showsOnFilterFeedbackBar):
return showsOnFilterFeedbackBar
case .listPicker(_, let showsOnFilterFeedbackBar):
return showsOnFilterFeedbackBar
}
}

Expand Down Expand Up @@ -101,6 +107,11 @@ public enum SortFilterItem: Identifiable, Hashable {
hasher.combine(item.originalValue)
hasher.combine(item.workingValue)
hasher.combine(item.value)
case .listPicker(let item, _):
hasher.combine(item.id)
hasher.combine(item.originalValue)
hasher.combine(item.workingValue)
hasher.combine(item.value)
}
}
}
Expand Down Expand Up @@ -206,6 +217,26 @@ extension SortFilterItem {
}
}

var listPicker: ListPickerItem {
get {
switch self {
case .listPicker(let item, _):
return item
default:
fatalError("Unexpected value \(self)")
}
}

set {
switch self {
case .listPicker(_, let showsOnFilterFeedbackBar):
self = .listPicker(item: newValue, showsOnFilterFeedbackBar: showsOnFilterFeedbackBar)
default:
fatalError("Unexpected value \(self)")
}
}
}

var isChanged: Bool {
switch self {
case .picker(let item, _):
Expand All @@ -218,6 +249,8 @@ extension SortFilterItem {
return item.isChanged
case .slider(let item, _):
return item.isChanged
case .listPicker(let item, _):
return item.isChanged
}
}

Expand All @@ -233,6 +266,8 @@ extension SortFilterItem {
return item.isOriginal
case .slider(let item, _):
return item.isOriginal
case .listPicker(let item, _):
return item.isOriginal
}
}

Expand All @@ -253,6 +288,9 @@ extension SortFilterItem {
case .slider(var item, _):
item.cancel()
self.slider = item
case .listPicker(var item, _):
item.cancel()
self.listPicker = item
}
}

Expand All @@ -273,6 +311,9 @@ extension SortFilterItem {
case .slider(var item, _):
item.reset()
self.slider = item
case .listPicker(var item, _):
item.reset()
self.listPicker = item
}
}

Expand All @@ -293,6 +334,9 @@ extension SortFilterItem {
case .slider(var item, _):
item.apply()
self.slider = item
case .listPicker(var item, _):
item.apply()
self.listPicker = item
}
}
}
Expand All @@ -311,9 +355,23 @@ public extension SortFilterItem {
public let allowsEmptySelection: Bool
public var showsValueForSingleSelected: Bool = true
public let icon: String?
/// itemLayout is used when listPickerMode is filterFormCell, otherwise is ignored.
public var itemLayout: OptionListPickerItemLayoutType = .fixed

public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed) {
public var displayMode: DisplayMode = .automatic

/// Available OptionListPicker modes. Use this enum to define picker mode to present.
public enum DisplayMode {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nesting Violation: Types should be nested at most 1 level deep (nesting)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nesting Violation: Types should be nested at most 1 level deep (nesting)

/// Decided by options count
case automatic
/// FilterFormCell
case filterFormCell
/// Menu
case menu
/// List
case list
}

public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, displayMode: DisplayMode = .automatic) {
self.id = id
self.name = name
self.value = value
Expand All @@ -325,6 +383,7 @@ public extension SortFilterItem {
self.showsValueForSingleSelected = showsValueForSingleSelected
self.icon = icon
self.itemLayout = itemLayout
self.displayMode = displayMode
}

mutating func onTap(option: String) {
Expand Down Expand Up @@ -389,6 +448,15 @@ public extension SortFilterItem {
self.workingValue.contains(index)
}

mutating func selectAll(_ isAll: Bool) {
self.workingValue.removeAll()
if isAll {
for i in 0 ..< self.valueOptions.count {
self.workingValue.append(i)
}
}
}

var isChecked: Bool {
!self.value.isEmpty
}
Expand Down Expand Up @@ -582,4 +650,127 @@ public extension SortFilterItem {
self.workingValue == self.originalValue
}
}

/// Data structure for filter feedback, option list picker,
struct ListPickerItem: Identifiable, Equatable {
public let id: String
public var name: String
public var value: [Int]
public var workingValue: [Int]
let originalValue: [Int]

var valueOptions: [String]
public let allowsMultipleSelection: Bool
public let allowsEmptySelection: Bool
public var showsValueForSingleSelected: Bool = true
public let icon: String?

public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil) {
self.id = id
self.name = name
self.value = value
self.workingValue = value
self.originalValue = value
self.valueOptions = valueOptions
self.allowsMultipleSelection = allowsMultipleSelection
self.allowsEmptySelection = allowsEmptySelection
self.showsValueForSingleSelected = showsValueForSingleSelected
self.icon = icon
}

mutating func onTap(option: String) {
guard let index = valueOptions.firstIndex(of: option) else { return }
if self.workingValue.contains(index) {
if self.workingValue.count > 1 {
self.workingValue = self.workingValue.filter { $0 != index }
} else {
if self.allowsEmptySelection {
self.workingValue = []
} else {
self.workingValue = index == 1 ? [0] : [1]
}
}
} else {
if self.allowsMultipleSelection {
self.workingValue.append(index)
} else {
self.workingValue = [index]
}
}
}

mutating func optionOnTap(_ index: Int) {
if self.workingValue.contains(index) {
if self.workingValue.count > 1 {
self.workingValue = self.workingValue.filter { $0 != index }
} else {
if self.allowsEmptySelection {
self.workingValue = []
} else {
self.workingValue = index == 1 ? [0] : [1]
}
}
} else {
if self.allowsMultipleSelection {
self.workingValue.append(index)
} else {
self.workingValue = [index]
}
}
}

mutating func cancel() {
self.workingValue = self.value.map { $0 }
}

mutating func reset() {
self.workingValue = self.originalValue.map { $0 }
}

mutating func apply() {
self.value = self.workingValue.map { $0 }
}

func isOptionSelected(_ option: String) -> Bool {
guard let idx = valueOptions.firstIndex(of: option) else { return false }
return self.workingValue.contains(idx)
}

func isOptionSelected(index: Int) -> Bool {
self.workingValue.contains(index)
}

mutating func selectAll(_ isAll: Bool) {
self.workingValue.removeAll()
if isAll {
for i in 0 ..< self.valueOptions.count {
self.workingValue.append(i)
}
}
}

var isChecked: Bool {
!self.value.isEmpty
}

var label: String {
if self.allowsMultipleSelection, self.value.count >= 1 {
if self.value.count == 1, self.showsValueForSingleSelected {
return self.valueOptions[self.value[0]]
} else {
return "\(self.name) (\(self.value.count))"
}
} else {
return self.name
}
}

var isChanged: Bool {
self.value != self.workingValue
}

var isOriginal: Bool {
self.workingValue == self.originalValue
}
}
}
Loading
Loading