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(List): support custom sorting functions #1220

Merged
merged 11 commits into from
Feb 28, 2025
2 changes: 2 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
"@mittwald/flow-react-components": "workspace:*",
"@next/mdx": "^15.1.7",
"@tabler/icons-react": "^3.30.0",
"@types/luxon": "^3.4.2",
"@types/mdx": "^2.0.13",
"acorn": "8.11.2",
"acorn-typescript": "^1.4.13",
"clsx": "^2.1.1",
"dot-prop": "^9.0.0",
"fs-jetpack": "^5.1.0",
"humanize-string": "^3.0.0",
"luxon": "^3.5.0",
"next": "~15.1.7",
"next-mdx-remote": "^5.0.0",
"parse-es-import": "^0.6.0",
Expand Down
5 changes: 2 additions & 3 deletions apps/docs/src/app/_components/layout/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"use client";
import type { FC } from "react";
import React from "react";
import {
ColumnLayout,
Image,
LayoutCard,
Link,
} from "@mittwald/flow-react-components";
import styles from "./footer.module.scss";
import type { FC } from "react";
import feedback from "../../../../../assets/Styleguide-Footer_Feedback.svg";
import logoMittwald from "../../../../../assets/mittwald-logo-footer.svg";
import { FooterSection } from "./components/FooterSection";
import styles from "./footer.module.scss";

const Footer: FC = () => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import {
type Domain,
domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import {
AlertBadge,
Avatar,
ContextMenu,
Heading,
IconDomain,
IconSubdomain,
MenuItem,
type SortingFn,
SortingFunctions,
Text,
typedList,
} from "@mittwald/flow-react-components";
import { DateTime } from "luxon";

export default () => {
type DomainWithBigIntId = Omit<Domain, "id"> & {
id: bigint;
createdAt: DateTime;
};

const domainsWithDateTime = domains.map(
(domain, index) => {
const daysAgo =
index * 3 + Math.floor(Math.random() * 5);

const bigIntId = BigInt(1000000000000 + index);

return {
...domain,
id: bigIntId,
createdAt: DateTime.now().minus({ days: daysAgo }),
};
},
);

const DomainList = typedList<DomainWithBigIntId>();

const bigIntSorting =
SortingFunctions.bigInt as SortingFn<DomainWithBigIntId>;
const dateTimeSorting =
SortingFunctions.dateTime as SortingFn<DomainWithBigIntId>;

return (
<DomainList.List batchSize={5}>
<DomainList.StaticData data={domainsWithDateTime} />

<DomainList.Sorting
property="hostname"
name="Name A bis Z"
direction="asc"
/>
<DomainList.Sorting
property="hostname"
name="Name Z bis A"
direction="desc"
/>

<DomainList.Sorting
property="id"
name="ID (aufsteigend)"
direction="asc"
customSortingFn={bigIntSorting}
/>
<DomainList.Sorting
property="id"
name="ID (absteigend)"
direction="desc"
customSortingFn={bigIntSorting}
defaultEnabled
/>

<DomainList.Sorting
property="createdAt"
name="Erstellt am (älteste zuerst)"
direction="asc"
customSortingFn={dateTimeSorting}
/>
<DomainList.Sorting
property="createdAt"
name="Erstellt am (neueste zuerst)"
direction="desc"
customSortingFn={dateTimeSorting}
/>

<DomainList.Sorting
property="tld"
name="TLD-Länge (kürzeste zuerst)"
direction="asc"
customSortingFn={(rowA, rowB, columnId: string) => {
const tldA = String(
rowA.getValue(columnId) || "",
);
const tldB = String(
rowB.getValue(columnId) || "",
);
return tldA.length - tldB.length;
}}
/>

<DomainList.Item>
{(domain) => (
<DomainList.ItemView>
<Avatar
color={
domain.type === "Domain" ? "blue" : "teal"
}
>
{domain.type === "Domain" ? (
<IconDomain />
) : (
<IconSubdomain />
)}
</Avatar>
<Heading>
{domain.hostname}
{!domain.verified && (
<AlertBadge status="warning">
Unverifiziert
</AlertBadge>
)}
</Heading>
<Text>{domain.type}</Text>
<Text>ID: {domain.id}</Text>
<Text>TLD: {domain.tld}</Text>
<Text>
Erstellt am:{" "}
{domain.createdAt.toLocaleString()}
</Text>

<ContextMenu>
<MenuItem>Details anzeigen</MenuItem>
<MenuItem>Löschen</MenuItem>
</ContextMenu>
</DomainList.ItemView>
)}
</DomainList.Item>
</DomainList.List>
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
type Domain,
domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import {
AlertBadge,
Avatar,
Expand All @@ -9,10 +13,6 @@ import {
Text,
typedList,
} from "@mittwald/flow-react-components";
import {
type Domain,
domains,
} from "@/content/03-components/structure/list/examples/domainApi";

export default () => {
const DomainList = typedList<Domain>();
Expand Down
10 changes: 10 additions & 0 deletions apps/docs/src/content/03-components/structure/list/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,13 @@ beiden Darstellungen gewechselt werden.
angepasst werden.

<LiveCodeEditor example="tile" editorCollapsed />

## Mit einer eigenen Sortierfunktion

Benutzerdefinierte Sortierfunktionen ermöglichen spezielle Sortierlogik für
verschiedene Datentypen wie BigInt oder Datumsangaben mit luxon DateTime,
inklusive Typprüfung und Fallback-Mechanismen für ungültige Eingaben. Außerdem
können eigene Sortierfunktionen über die List.Sorting-Komponente mit dem
Property `customSortingFn` hinzugefügt werden.

<LiveCodeEditor example="customSorting" editorCollapsed />
12 changes: 7 additions & 5 deletions apps/docs/src/lib/PropertiesTables/PropertiesTables.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";
import { PropertiesTable } from "@/lib/PropertiesTables/components/PropertiesTable";
import {
Accordion,
Content,
Heading,
useIsMounted,
} from "@mittwald/flow-react-components";
import React from "react";
import loadProperties from "./lib/loadProperties";
import { PropertiesTable } from "@/lib/PropertiesTables/components/PropertiesTable";
import { Accordion } from "@mittwald/flow-react-components";
import { Heading } from "@mittwald/flow-react-components";
import { Content } from "@mittwald/flow-react-components";
import { useIsMounted } from "@mittwald/flow-react-components";

interface PropertiesTableProps {
name: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/lib/PropertiesTables/lib/loadProperties.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import docGenFile from "@mittwald/flow-react-components/doc-properties";
import type { Properties, Property } from "../types";
import type { ComponentDoc } from "react-docgen-typescript";
import type { Properties, Property } from "../types";

const eventRegex = /^on[A-Z]+.*/;
const a11yRegex = /^aria-.+/;
Expand Down
9 changes: 7 additions & 2 deletions packages/components/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { dirname, join } from "path";
import type { StorybookConfig } from "@storybook/react-vite";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import viteCheckerPlugin from "vite-plugin-checker";

function getAbsolutePath<T extends string>(value: T): T {
return dirname(require.resolve(join(value, "package.json"))) as T;
return join(
dirname(fileURLToPath(import.meta.url)),
"../node_modules",
value,
) as T;
}

const config: StorybookConfig = {
Expand Down
6 changes: 4 additions & 2 deletions packages/components/src/components/List/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export type {
DataLoaderResult,
} from "@/components/List/model/loading/types";

export type { SortingFn } from "@/components/List/model/sorting/types";

export { SortingFunctions } from "./model/sorting/SortingFunctions";

export * from "./components/Items/views/GridList";
export * from "./components/Items/views/GridListItem";
export * from "./components/ListItemView";
Expand All @@ -22,5 +26,3 @@ export * from "./setupComponents/ListItem";
export * from "./setupComponents/ListLoaderAsync";
export * from "./setupComponents/ListSorting";
export * from "./setupComponents/ListStaticData";

export * from "./setupComponents/ListStaticData";
18 changes: 12 additions & 6 deletions packages/components/src/components/List/model/sorting/Sorting.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
import type {
Column,
ColumnDef,
ColumnSort,
SortDirection,
} from "@tanstack/react-table";
import type List from "@/components/List/model/List";
import type {
SortingDefaultMode,
SortingShape,
} from "@/components/List/model/sorting/types";
import type { PropertyName } from "@/components/List/model/types";
import type {
Column,
ColumnDef,
ColumnSort,
SortDirection,
SortingFn,
} from "@tanstack/react-table";

export class Sorting<T> {
public readonly list: List<T>;
public readonly property: PropertyName<T>;
public readonly name?: string;
public readonly direction: SortDirection;
public readonly defaultEnabled: SortingDefaultMode;
public readonly customSortingFn?: SortingFn<T>;

public constructor(list: List<T>, shape: SortingShape<T>) {
this.list = list;
this.property = shape.property;
this.name = shape.name;
this.direction = shape.direction ?? "asc";
this.defaultEnabled = shape.defaultEnabled ?? false;
this.customSortingFn = shape.customSortingFn;
}

public updateTableColumnDef(def: ColumnDef<T>): void {
def.enableSorting = true;
if (this.customSortingFn) {
def.sortingFn = this.customSortingFn;
}
}

public getReactTableColumnSort(): ColumnSort {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Row, SortingFn } from "@tanstack/react-table";
import invariant from "invariant";
import { DateTime } from "luxon";

export const SortingFunctions = {
bigInt: ((rowA: Row<bigint>, rowB: Row<bigint>, columnId) => {
const valueA = rowA.getValue(columnId);
const valueB = rowB.getValue(columnId);

if (valueA == null) return valueB == null ? 0 : -1;
if (valueB == null) return 1;

try {
invariant(
typeof valueA === "bigint" && typeof valueB === "bigint",
`Expected BigInt values, got ${typeof valueA} and ${typeof valueB}`,
);

return valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
} catch (error) {
console.error(`Error in BigInt comparison: ${error}`);
}
}) as SortingFn<unknown>,

alphanumeric: "alphanumeric" as const,

dateTime: ((rowA: Row<unknown>, rowB: Row<unknown>, columnId) => {
const valueA = rowA.getValue(columnId);
const valueB = rowB.getValue(columnId);

if (valueA == null) return valueB == null ? 0 : -1;
if (valueB == null) return 1;

let dtA: DateTime | null = null;
let dtB: DateTime | null = null;

if (valueA instanceof DateTime) {
dtA = valueA;
} else if (typeof valueA === "string") {
dtA = DateTime.fromISO(valueA);
}

if (valueB instanceof DateTime) {
dtB = valueB;
} else if (typeof valueB === "string") {
dtB = DateTime.fromISO(valueB);
}

if (dtA?.isValid && dtB?.isValid) {
return dtA.toMillis() - dtB.toMillis();
} else if (dtA?.isValid) {
return -1;
} else if (dtB?.isValid) {
return 1;
} else {
console.warn("Invalid DateTime values for sorting.");
return 0;
}
}) as SortingFn<unknown>,
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import type { SortDirection } from "@tanstack/react-table";
import type { PropertyName } from "@/components/List/model/types";
import type {
SortDirection,
SortingFn as SortingFunction,
} from "@tanstack/react-table";

export type SortingDefaultMode = boolean | "hidden";

export type SortingFn<T> = SortingFunction<T>;

export interface SortingShape<T> {
property: PropertyName<T>;
name?: string;
direction?: SortDirection;
defaultEnabled?: SortingDefaultMode;
customSortingFn?: SortingFn<T>;
}
Loading
Loading