@linagora/twake-mui 13.2.1 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # @linagora/twake-mui [14.1.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@14.0.0...@linagora/twake-mui@14.1.0) (2026-10-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * **twake-mui:** Migrate LoadMore from cozy-ui ([3b505ec](https://github.com/linagora/twake-ui/commit/3b505ec16434d371d7a730cc314df06e6013b724))
7
+
8
+ # @linagora/twake-mui [14.0.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@13.2.1...@linagora/twake-mui@14.0.0) (2026-10-15)
9
+
10
+
11
+ ### Code Refactoring
12
+
13
+ * **twake-mui:** Remove secondarySort from VirtualizedTable ([f0d5632](https://github.com/linagora/twake-ui/commit/f0d5632c57346428f5075f7ea9e338128a597c48))
14
+
15
+
16
+ ### Features
17
+
18
+ * **twake-mui:** Add sortingMode to VirtualizedTable ([e747721](https://github.com/linagora/twake-ui/commit/e74772185ae3447d0fce935393119e785b3a3c3a))
19
+
20
+
21
+ ### BREAKING CHANGES
22
+
23
+ * **twake-mui:** VirtualizedTable no longer takes secondarySort. Sort
24
+ the rows, secondary order included, before passing them, and set
25
+ sortingMode="server" so that the table keeps their order.
26
+
27
+ Assisted-by: LLM
28
+
1
29
  ## @linagora/twake-mui [13.2.1](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@13.2.0...@linagora/twake-mui@13.2.1) (2026-10-13)
2
30
 
3
31
 
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import LoadMore from './index';
3
+ declare const meta: Meta<typeof LoadMore>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const Default: Story;
7
+ export declare const Screenshot: Story;
@@ -0,0 +1,43 @@
1
+ import { Box, Stack, Typography } from '@mui/material';
2
+ import React, { useState } from 'react';
3
+ import LoadMore from './index';
4
+ const sleep = (timeout) => new Promise(resolve => setTimeout(resolve, timeout));
5
+ const InfiniteList = ({ fetchMore: onFetchMore, ...props }) => {
6
+ const [elements, setElements] = useState([1, 2]);
7
+ const fetchMore = async () => {
8
+ onFetchMore?.();
9
+ await sleep(1000);
10
+ setElements(previous => [...previous, previous.length + 1]);
11
+ };
12
+ return (React.createElement(Box, { sx: { height: 300, overflow: 'auto' } },
13
+ React.createElement(Typography, null, "Scroll down to load more."),
14
+ elements.map(element => (React.createElement(Box, { key: element, sx: { bgcolor: 'grey.500', height: 160, mb: 1 } }))),
15
+ elements.length < 25 && React.createElement(LoadMore, { fetchMore: fetchMore, ...props })));
16
+ };
17
+ const Section = ({ title, children }) => (React.createElement("section", null,
18
+ React.createElement("h3", null, title),
19
+ children));
20
+ const meta = {
21
+ title: 'LoadMore',
22
+ component: LoadMore,
23
+ tags: ['autodocs'],
24
+ argTypes: {
25
+ label: { control: 'text' },
26
+ fetchMore: { action: 'fetchMore' }
27
+ }
28
+ };
29
+ export default meta;
30
+ export const Default = {
31
+ args: {
32
+ label: 'Load more'
33
+ },
34
+ render: args => React.createElement(InfiniteList, { ...args })
35
+ };
36
+ export const Screenshot = {
37
+ tags: ['argos'],
38
+ render: () => (React.createElement(Stack, { spacing: 4 },
39
+ React.createElement(Section, { title: "Button" },
40
+ React.createElement(LoadMore, { label: "Load more", fetchMore: () => { } })),
41
+ React.createElement(Section, { title: "Loading" },
42
+ React.createElement(LoadMore, { label: "Load more", fetchMore: () => new Promise(() => { }) }))))
43
+ };
@@ -0,0 +1,9 @@
1
+ import { ButtonProps } from '@mui/material';
2
+ import React from 'react';
3
+ export interface LoadMoreProps extends Omit<ButtonProps, 'children' | 'onClick' | 'loading'> {
4
+ /** Called when the next batch of data needs to be loaded. Can return a promise. */
5
+ fetchMore: () => unknown;
6
+ label: React.ReactNode;
7
+ }
8
+ declare const LoadMore: React.FC<LoadMoreProps>;
9
+ export default LoadMore;
@@ -0,0 +1,30 @@
1
+ import { Button, CircularProgress } from '@mui/material';
2
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
3
+ const LoadMore = ({ fetchMore, label, ...props }) => {
4
+ const [isLoading, setIsLoading] = useState(false);
5
+ const elementRef = useRef(null);
6
+ const startFetchMore = useCallback(async () => {
7
+ if (isLoading)
8
+ return;
9
+ setIsLoading(true);
10
+ await fetchMore();
11
+ setIsLoading(false);
12
+ }, [isLoading, fetchMore]);
13
+ const handleClick = () => {
14
+ void startFetchMore();
15
+ };
16
+ useEffect(() => {
17
+ const element = elementRef.current;
18
+ if (!element)
19
+ return;
20
+ const observer = new IntersectionObserver((entries) => {
21
+ if (entries.some(entry => entry.isIntersecting))
22
+ void startFetchMore();
23
+ });
24
+ observer.observe(element);
25
+ return () => observer.disconnect();
26
+ }, [startFetchMore]);
27
+ return (React.createElement("span", { ref: elementRef },
28
+ React.createElement(Button, { variant: "text", onClick: handleClick, ...props }, isLoading ? React.createElement(CircularProgress, { size: 16 }) : label)));
29
+ };
30
+ export default LoadMore;
@@ -4,4 +4,5 @@ declare const meta: Meta<typeof VirtualizedTable>;
4
4
  export default meta;
5
5
  type Story = StoryObj<typeof meta>;
6
6
  export declare const Default: Story;
7
+ export declare const ServerSortingMode: Story;
7
8
  export declare const Screenshot: Story;
@@ -1,6 +1,7 @@
1
1
  import { Button, Skeleton, Typography } from '@mui/material';
2
2
  import React, { useState } from 'react';
3
3
  import { fn } from 'storybook/test';
4
+ import { getComparator, stableSort } from './helpers';
4
5
  import { VirtualizedTable } from './index';
5
6
  const createData = (id, name, calories, fat, carbs, protein) => ({ id, name, calories, fat, carbs, protein });
6
7
  const rows = [
@@ -76,10 +77,10 @@ const meta = {
76
77
  rows: { control: 'object' },
77
78
  columns: { control: 'object' },
78
79
  defaultOrder: { control: 'object' },
80
+ sortingMode: { control: 'inline-radio', options: ['client', 'server'] },
79
81
  selectedItems: { control: 'object' },
80
82
  isLoading: { control: 'boolean' },
81
83
  groups: { control: false },
82
- secondarySort: { control: false },
83
84
  isSelectedItem: { control: false },
84
85
  isNewItem: { control: false },
85
86
  context: { control: false },
@@ -96,6 +97,14 @@ export const Default = {
96
97
  },
97
98
  render: args => React.createElement(ExampleTable, { title: "Table", ...args })
98
99
  };
100
+ // The caller sorts the rows, as a server would, and the table keeps their order
101
+ const SortedByCallerTable = () => {
102
+ const [sortedRows, setSortedRows] = useState(rows);
103
+ return (React.createElement(ExampleTable, { title: "Rows sorted by the caller", rows: sortedRows, sortingMode: "server", onSortChange: ({ order, orderBy }) => setSortedRows(stableSort(rows, getComparator(order, orderBy))) }));
104
+ };
105
+ export const ServerSortingMode = {
106
+ render: () => React.createElement(SortedByCallerTable, null)
107
+ };
99
108
  // Visual Regression - Combined view for Argos testing
100
109
  export const Screenshot = {
101
110
  tags: ['argos'],
@@ -12,13 +12,18 @@ export interface VirtualizedTableProps extends Omit<TableVirtuosoProps<Row, Tabl
12
12
  groupLabels: React.ReactNode[];
13
13
  groupCounts: number[];
14
14
  };
15
- /** Default sorting configuration */
15
+ /** Column sorted at first: without it, no column is sorted until one is clicked */
16
16
  defaultOrder?: {
17
17
  direction?: OrderDirection;
18
18
  by?: string;
19
19
  };
20
- /** Applied after the column sort, e.g. to keep directories before files */
21
- secondarySort?: (rows: Row[]) => Row[];
20
+ /**
21
+ * Who sorts the rows. 'server': the table keeps the given order, its
22
+ * headers changing the sort and calling onSortChange for the caller to sort
23
+ * them, e.g. on its server. 'client': the table sorts them by the sorted
24
+ * column
25
+ */
26
+ sortingMode?: 'client' | 'server';
22
27
  /** Array of selected items */
23
28
  selectedItems?: unknown[];
24
29
  /** Function to determine if a row is selected */
@@ -6,7 +6,7 @@ import { RowContent } from './RowContent';
6
6
  import { getComparator, stableSort } from './helpers';
7
7
  import { _isLoading } from './types';
8
8
  import { virtuosoComponents } from './virtuosoComponents';
9
- export const VirtualizedTable = forwardRef(({ rows, columns, groups, defaultOrder, secondarySort, selectedItems = [], isSelectedItem = () => false, isNewItem, context, componentsProps, components = virtuosoComponents, onSortChange, isLoading, skeletonCount = 10, ...props }, ref) => {
9
+ export const VirtualizedTable = forwardRef(({ rows, columns, groups, defaultOrder, sortingMode = 'client', selectedItems = [], isSelectedItem = () => false, isNewItem, context, componentsProps, components = virtuosoComponents, onSortChange, isLoading, skeletonCount = 10, ...props }, ref) => {
10
10
  const [orderDirection, setOrderDirection] = useState(defaultOrder?.direction ?? 'asc');
11
11
  const [orderBy, setOrderBy] = useState(defaultOrder?.by);
12
12
  const data = useMemo(() => {
@@ -16,11 +16,10 @@ export const VirtualizedTable = forwardRef(({ rows, columns, groups, defaultOrde
16
16
  id: `skeleton-${i}`
17
17
  }));
18
18
  }
19
- const sortedRows = orderBy
19
+ return orderBy && sortingMode === 'client'
20
20
  ? stableSort(rows, getComparator(orderDirection, orderBy))
21
21
  : rows;
22
- return secondarySort ? secondarySort(sortedRows) : sortedRows;
23
- }, [rows, orderBy, orderDirection, secondarySort, isLoading, skeletonCount]);
22
+ }, [rows, orderBy, orderDirection, sortingMode, isLoading, skeletonCount]);
24
23
  const { groupLabels, groupCounts } = (isLoading ? undefined : groups?.(data)) ?? {};
25
24
  const tableContext = {
26
25
  ...context,
package/dist/index.d.ts CHANGED
@@ -52,6 +52,7 @@ export { default as ListItemText } from './components/ListItemText';
52
52
  export { default as ListItemSkeleton } from './components/ListItemSkeleton';
53
53
  export { default as ListSkeleton } from './components/ListSkeleton';
54
54
  export { default as ListSubheader } from './components/ListSubheader';
55
+ export { default as LoadMore } from './components/LoadMore';
55
56
  export { default as Markdown } from './components/Markdown';
56
57
  export { default as MidEllipsis } from './components/MidEllipsis';
57
58
  export { default as PointerAlert } from './components/PointerAlert';
@@ -130,6 +131,7 @@ export type { ListItemTextProps } from './components/ListItemText';
130
131
  export type { ListItemSkeletonProps } from './components/ListItemSkeleton';
131
132
  export type { ListSkeletonProps } from './components/ListSkeleton';
132
133
  export type { ListSubheaderProps } from './components/ListSubheader';
134
+ export type { LoadMoreProps } from './components/LoadMore';
133
135
  export type { MarkdownProps } from './components/Markdown';
134
136
  export type { MidEllipsisProps } from './components/MidEllipsis';
135
137
  export type { TabsProps } from './components/Tabs';
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ export { default as ListItemText } from './components/ListItemText';
55
55
  export { default as ListItemSkeleton } from './components/ListItemSkeleton';
56
56
  export { default as ListSkeleton } from './components/ListSkeleton';
57
57
  export { default as ListSubheader } from './components/ListSubheader';
58
+ export { default as LoadMore } from './components/LoadMore';
58
59
  export { default as Markdown } from './components/Markdown';
59
60
  export { default as MidEllipsis } from './components/MidEllipsis';
60
61
  export { default as PointerAlert } from './components/PointerAlert';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linagora/twake-mui",
3
3
  "type": "module",
4
- "version": "13.2.1",
4
+ "version": "14.1.0",
5
5
  "description": "MUI v9 theme system for Twake applications",
6
6
  "author": "Twake",
7
7
  "license": "MIT",