Skip to content

Commit

Permalink
[CP-3166] Enhance Sorting Logic with Grouping, Null Handling, and Cas…
Browse files Browse the repository at this point in the history
…e Insensitivity (#2116)
  • Loading branch information
dkarski authored Oct 25, 2024
1 parent d5ac886 commit 2bcb429
Show file tree
Hide file tree
Showing 15 changed files with 827 additions and 256 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ describe("dataProviderSchema", () => {
entitiesType: "someType",
sort: [
{
providerField: "someField",
field: "someField",
direction: "asc",
priority: 1,
orderingPatterns: ["/pattern/"],
},
],
filters: [
{
providerField: "someField",
field: "someField",
patterns: ["/pattern/"],
},
],
Expand Down
39 changes: 31 additions & 8 deletions libs/device/models/src/lib/feature/data-provider-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,46 @@ export type DataProviderField =
| z.infer<typeof enhancedFieldSchema>
| z.infer<typeof superEnhancedFieldSchema>

const sortDirectionSchema = z.union([z.literal("asc"), z.literal("desc")])

export type SortDirection = z.infer<typeof sortDirectionSchema>

const sortOrderingPatternsSchema = z.array(regexSchema)

export type SortOrderingPatterns = z.infer<typeof sortOrderingPatternsSchema>

// Types come from Intl.CollatorOptions["sensitivity"], used to control text comparison sensitivity
const sortSensitivitySchema = z.enum(["base", "accent", "case", "variant"])

export type SortSensitivity = z.infer<typeof sortSensitivitySchema>

const emptyOrderSchema = z.enum(["first", "last"])

const sortSchema = z
.array(
z.object({
providerField: z.string(),
priority: z.number().nonnegative(),
direction: z.union([z.literal("asc"), z.literal("desc")]),
orderingPatterns: z.array(regexSchema).optional(),
})
z
.object({
field: z.string().optional(),
fieldGroup: z.array(z.string()).optional(),
priority: z.number().nonnegative(),
direction: sortDirectionSchema,
orderingPatterns: sortOrderingPatternsSchema.optional(),
sensitivity: sortSensitivitySchema.optional(),
emptyOrder: emptyOrderSchema.optional(),
})
.refine((data) => data.field || data.fieldGroup, {
message: "Either field or fieldGroup must be provided",
path: ["field", "fieldGroup"],
})
)
.optional()

export type DataProviderSortConfig = z.infer<typeof sortSchema>
export type DataSortConfig = z.infer<typeof sortSchema>

const filtersSchema = z
.array(
z.object({
providerField: z.string(),
field: z.string(),
patterns: z.array(regexSchema),
})
)
Expand Down
8 changes: 4 additions & 4 deletions libs/generic-view/feature/src/lib/setup-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
useFormField,
} from "generic-view/store"
import {
dataProviderFilter,
dataProviderSort,
dataFilter,
dataSort,
mapLayoutSizes,
RecursiveComponent,
useViewFormContext,
Expand Down Expand Up @@ -120,11 +120,11 @@ export const setupComponent = <P extends object>(
})

if (dataProvider?.source === "entities-array") {
const filteredData = dataProviderFilter(
const filteredData = dataFilter(
[...entitiesData],
dataProvider.filters
)
const sortedData = dataProviderSort([...filteredData], dataProvider.sort)
const sortedData = dataSort([...filteredData], dataProvider.sort)
editableProps.data = sortedData?.map((item) => item[idFieldKey!])
} else if (dataProvider?.source === "entities-field") {
if (entityData) {
Expand Down
5 changes: 3 additions & 2 deletions libs/generic-view/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export * from "./lib/view-generators/generate-view-config"
export * from "./lib/map-layout-sizes/map-layout-sizes"
export * from "./lib/models/modal.types"
export * from "./lib/get-base-device-info"
export * from "./lib/data-provider-helpers"
export * from "./lib/data-filter"
export * from "./lib/data-sort"
export * from "./lib/forms-provider/forms-provider"
export * from "./lib/data-provider-helpers/string-to-regex"
export * from "./lib/string-to-regex/string-to-regex"
export * from "./lib/use-current-view-key"
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,40 @@
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

import { dataProviderFilter } from "./data-provider-filter"
import { dataFilter } from "./data-filter"
import { DataProviderFiltersConfig } from "device/models"

describe("dataProviderFilter", () => {
it("returns all data when no filters are provided", () => {
const data = [{ name: "Alice" }, { name: "Bob" }]
const result = dataProviderFilter(data)
const result = dataFilter(data)
expect(result).toEqual(data)
})

it("filters data based on single field with single pattern", () => {
const data = [{ name: "Alice" }, { name: "Bob" }]
const filters: DataProviderFiltersConfig = [
{ providerField: "name", patterns: ["/Alice/"] },
{ field: "name", patterns: ["/Alice/"] },
]
const result = dataProviderFilter(data, filters)
const result = dataFilter(data, filters)
expect(result).toEqual([{ name: "Alice" }])
})

it("filters data based on nested field with single pattern", () => {
const data = [{ user: { name: "Alice" } }, { user: { name: "Bob" } }]
const filters: DataProviderFiltersConfig = [
{ providerField: "user.name", patterns: ["/Alice/"] },
{ field: "user.name", patterns: ["/Alice/"] },
]
const result = dataProviderFilter(data, filters)
const result = dataFilter(data, filters)
expect(result).toEqual([{ user: { name: "Alice" } }])
})

it("filters data based on single field with multiple patterns", () => {
const data = [{ name: "Alice" }, { name: "Anastasia" }, { name: "Charlie" }]
const filters: DataProviderFiltersConfig = [
{ providerField: "name", patterns: ["/^A/m", "/.+e$/m"] },
{ field: "name", patterns: ["/^A/m", "/.+e$/m"] },
]
const result = dataProviderFilter(data, filters)
const result = dataFilter(data, filters)
expect(result).toEqual([{ name: "Alice" }])
})

Expand All @@ -47,10 +47,10 @@ describe("dataProviderFilter", () => {
{ name: "Agnes", age: "30" },
]
const filters: DataProviderFiltersConfig = [
{ providerField: "name", patterns: ["/^A/m"] },
{ providerField: "age", patterns: ["/2[\\d]/"] },
{ field: "name", patterns: ["/^A/m"] },
{ field: "age", patterns: ["/2[\\d]/"] },
]
const result = dataProviderFilter(data, filters)
const result = dataFilter(data, filters)
expect(result).toEqual([
{ name: "Alice", age: "25" },
{ name: "Anastasia", age: "29" },
Expand All @@ -60,39 +60,39 @@ describe("dataProviderFilter", () => {
it("returns empty array when no data matches the filters", () => {
const data = [{ name: "Alice" }, { name: "Bob" }]
const filters: DataProviderFiltersConfig = [
{ providerField: "name", patterns: ["/Charlie/"] },
{ field: "name", patterns: ["/Charlie/"] },
]
const result = dataProviderFilter(data, filters)
const result = dataFilter(data, filters)
expect(result).toEqual([])
})

it("handles empty data array", () => {
const data: Record<string, unknown>[] = []
const filters: DataProviderFiltersConfig = [
{ providerField: "name", patterns: ["/Alice/"] },
{ field: "name", patterns: ["/Alice/"] },
]
const result = dataProviderFilter(data, filters)
const result = dataFilter(data, filters)
expect(result).toEqual([])
})

it("handles undefined data", () => {
const filters: DataProviderFiltersConfig = [
{ providerField: "name", patterns: ["/Alice/"] },
{ field: "name", patterns: ["/Alice/"] },
]
const result = dataProviderFilter(undefined, filters)
const result = dataFilter(undefined, filters)
expect(result).toEqual([])
})

it("handles undefined filters", () => {
const data = [{ name: "Alice" }, { name: "Bob" }]
const result = dataProviderFilter(data, undefined)
const result = dataFilter(data, undefined)
expect(result).toEqual(data)
})

it("handles empty filters", () => {
const data = [{ name: "Alice" }, { name: "Bob" }]
const filters = [] as DataProviderFiltersConfig
const result = dataProviderFilter(data, filters)
const result = dataFilter(data, filters)
expect(result).toEqual(data)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
*/

import { DataProviderFiltersConfig } from "device/models"
import { stringToRegex } from "./string-to-regex"
import { stringToRegex } from "../string-to-regex/string-to-regex"
import { cloneDeep, get } from "lodash"

export const dataProviderFilter = (
export const dataFilter = (
data: Record<string, unknown>[] = [],
filters?: DataProviderFiltersConfig
) => {
if (!filters || !data) return data

return data.filter((item) => {
return cloneDeep(filters).every(({ providerField, patterns }) => {
const field = get(item, providerField) as string
return cloneDeep(filters).every(({ field, patterns }) => {
const value = get(item, field) as string
return patterns.every((pattern) => {
const regex = stringToRegex(pattern)
return regex.test(field || "")
return regex.test(value || "")
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

export * from "./data-provider-sort"
export * from "./data-provider-filter"
export * from "./data-filter"
Loading

0 comments on commit 2bcb429

Please sign in to comment.