@pallasoft/ps-mobile-components 1.5.0 → 1.6.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.
Files changed (33) hide show
  1. package/dist/components/ps-infinite-list/index.d.ts +2 -0
  2. package/dist/components/ps-infinite-list/index.js +2 -0
  3. package/dist/components/ps-infinite-list/ps-infinite-list-footer.d.ts +2 -0
  4. package/dist/components/ps-infinite-list/ps-infinite-list-footer.js +5 -0
  5. package/dist/components/ps-infinite-list/ps-infinite-list-item.d.ts +2 -0
  6. package/dist/components/ps-infinite-list/ps-infinite-list-item.js +8 -0
  7. package/dist/components/ps-infinite-list/ps-infinite-list-props.d.ts +34 -0
  8. package/dist/components/ps-infinite-list/ps-infinite-list-props.js +1 -0
  9. package/dist/components/ps-infinite-list/ps-infinite-list.d.ts +2 -0
  10. package/dist/components/ps-infinite-list/ps-infinite-list.js +39 -0
  11. package/dist/constants/ps-infinite-config.d.ts +6 -0
  12. package/dist/constants/ps-infinite-config.js +6 -0
  13. package/dist/hooks/use-ps-infinite-data.d.ts +3 -0
  14. package/dist/hooks/use-ps-infinite-data.js +192 -0
  15. package/dist/index.d.ts +6 -0
  16. package/dist/index.js +6 -0
  17. package/dist/interfaces/ps-infinite-data-options.d.ts +10 -0
  18. package/dist/interfaces/ps-infinite-data-options.js +1 -0
  19. package/dist/interfaces/ps-infinite-data-result.d.ts +12 -0
  20. package/dist/interfaces/ps-infinite-data-result.js +1 -0
  21. package/dist/interfaces/ps-infinite-data-state.d.ts +8 -0
  22. package/dist/interfaces/ps-infinite-data-state.js +1 -0
  23. package/dist/interfaces/ps-infinite-data-update.d.ts +1 -0
  24. package/dist/interfaces/ps-infinite-data-update.js +1 -0
  25. package/dist/interfaces/ps-infinite-data-write.d.ts +6 -0
  26. package/dist/interfaces/ps-infinite-data-write.js +1 -0
  27. package/dist/interfaces/ps-infinite-page.d.ts +4 -0
  28. package/dist/interfaces/ps-infinite-page.js +1 -0
  29. package/dist/services/ps-infinite-data-service.d.ts +8 -0
  30. package/dist/services/ps-infinite-data-service.js +22 -0
  31. package/dist/services/ps-infinite-list-service.d.ts +10 -0
  32. package/dist/services/ps-infinite-list-service.js +36 -0
  33. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ export * from './ps-infinite-list';
2
+ export * from './ps-infinite-list-props';
@@ -0,0 +1,2 @@
1
+ export * from './ps-infinite-list';
2
+ export * from './ps-infinite-list-props';
@@ -0,0 +1,2 @@
1
+ import type { PsInfiniteListFooterProps } from './ps-infinite-list-props';
2
+ export declare const PsInfiniteListFooter: ({ isLoadingMore }: PsInfiniteListFooterProps) => import("react").JSX.Element | null;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { View } from 'react-native';
3
+ import { PsLayoutConfig } from '../../constants/ps-layout-config';
4
+ import { PsSpinner } from '../ps-spinner/ps-spinner';
5
+ export const PsInfiniteListFooter = ({ isLoadingMore }) => isLoadingMore ? (_jsx(View, { style: { alignItems: 'center', paddingVertical: PsLayoutConfig.gutter }, testID: 'ps-infinite-list-loading-more', children: _jsx(PsSpinner, {}) })) : null;
@@ -0,0 +1,2 @@
1
+ import type { PsInfiniteListItemProps } from './ps-infinite-list-props';
2
+ export declare const PsInfiniteListItem: ({ children, count, index }: PsInfiniteListItemProps) => import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { View } from 'react-native';
3
+ import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
4
+ import { PsInfiniteListService } from '../../services/ps-infinite-list-service';
5
+ export const PsInfiniteListItem = ({ children, count, index }) => {
6
+ const theme = usePsMobileTheme();
7
+ return (_jsxs(View, { style: PsInfiniteListService.itemFrame(theme, index, count), children: [index === 0 ? null : _jsx(View, { style: PsInfiniteListService.divider(theme) }), children] }));
8
+ };
@@ -0,0 +1,34 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ import type { PsListVariant } from '../../services/ps-list-service';
4
+ import type { PsListSkeletonLayout } from '../ps-list/ps-list-props';
5
+ export interface PsInfiniteSection<T> {
6
+ data: T[];
7
+ footer?: string;
8
+ key: string;
9
+ title?: string;
10
+ }
11
+ export interface PsInfiniteListProps<T> {
12
+ empty?: ReactNode;
13
+ hasMore: boolean;
14
+ header?: ReactNode;
15
+ isLoading?: boolean;
16
+ isLoadingMore?: boolean;
17
+ items?: T[];
18
+ keyExtractor: (item: T) => string;
19
+ onEndReached: () => void;
20
+ onRefresh?: () => Promise<void>;
21
+ renderItem: (item: T) => ReactElement;
22
+ sections?: PsInfiniteSection<T>[];
23
+ skeleton?: PsListSkeletonLayout;
24
+ style?: StyleProp<ViewStyle>;
25
+ variant?: PsListVariant;
26
+ }
27
+ export interface PsInfiniteListFooterProps {
28
+ isLoadingMore: boolean;
29
+ }
30
+ export interface PsInfiniteListItemProps {
31
+ children: ReactElement;
32
+ count: number;
33
+ index: number;
34
+ }
@@ -0,0 +1,2 @@
1
+ import type { PsInfiniteListProps } from './ps-infinite-list-props';
2
+ export declare const PsInfiniteList: <T>({ empty, hasMore, header, isLoading, isLoadingMore, items, keyExtractor, onEndReached, onRefresh, renderItem, sections, skeleton, style, variant, }: PsInfiniteListProps<T>) => import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Platform, RefreshControl, SectionList, View } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+ import { PsInfiniteConfig } from '../../constants/ps-infinite-config';
5
+ import { PsLayoutConfig } from '../../constants/ps-layout-config';
6
+ import { PsSelectionBarConfig } from '../../constants/ps-selection-bar-config';
7
+ import { PsScrollContext } from '../../contexts/ps-scroll-context';
8
+ import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
9
+ import { usePsRefresh } from '../../hooks/use-ps-refresh';
10
+ import { usePsScrollHost } from '../../hooks/use-ps-scroll-host';
11
+ import { usePsTabBar } from '../../hooks/use-ps-tab-bar';
12
+ import { usePsTopInset } from '../../hooks/use-ps-top-inset';
13
+ import { PsInfiniteListService } from '../../services/ps-infinite-list-service';
14
+ import { PsListService } from '../../services/ps-list-service';
15
+ import { PsScreenService } from '../../services/ps-screen-service';
16
+ import { PsSkeletonList } from '../ps-skeleton/ps-skeleton-list';
17
+ import { PsText } from '../ps-text/ps-text';
18
+ import { PsInfiniteListFooter } from './ps-infinite-list-footer';
19
+ import { PsInfiniteListItem } from './ps-infinite-list-item';
20
+ export const PsInfiniteList = ({ empty, hasMore, header, isLoading = false, isLoadingMore = false, items, keyExtractor, onEndReached, onRefresh, renderItem, sections, skeleton, style, variant = 'grouped', }) => {
21
+ const theme = usePsMobileTheme();
22
+ const insets = useSafeAreaInsets();
23
+ const topInset = usePsTopInset();
24
+ const { hasTabBar, isHidden: isTabBarHidden } = usePsTabBar();
25
+ const { isRefreshing, refresh } = usePsRefresh(onRefresh);
26
+ const host = usePsScrollHost();
27
+ const shown = isLoading ? [] : PsInfiniteListService.sections(items, sections);
28
+ return (_jsxs(View, { style: { flex: 1 }, children: [_jsx(PsScrollContext, { value: host.context, children: _jsx(SectionList, { ListEmptyComponent: isLoading ? _jsx(PsSkeletonList, { layout: skeleton }) : _jsx(_Fragment, { children: empty }), ListFooterComponent: _jsx(PsInfiniteListFooter, { isLoadingMore: isLoadingMore }), ListHeaderComponent: header === undefined ? undefined : (_jsx(View, { style: { marginBottom: PsLayoutConfig.listGap }, children: header })), contentContainerStyle: PsScreenService.content({
29
+ bottom: PsScreenService.bottomInset(insets.bottom, hasTabBar, isTabBarHidden),
30
+ top: topInset,
31
+ }, Platform.OS === 'ios', PsListService.hasGutter(variant), 0, isTabBarHidden ? PsSelectionBarConfig.height : 0), contentInsetAdjustmentBehavior: 'automatic', initialNumToRender: PsInfiniteConfig.initialNumToRender, keyExtractor: keyExtractor, keyboardDismissMode: 'interactive', keyboardShouldPersistTaps: 'handled', maxToRenderPerBatch: PsInfiniteConfig.maxToRenderPerBatch, onContentSizeChange: host.onContentSizeChange, onEndReached: () => {
32
+ if (PsInfiniteListService.shouldLoadMore(hasMore, isLoading, isLoadingMore))
33
+ onEndReached();
34
+ }, onEndReachedThreshold: PsInfiniteConfig.endReachedThreshold, onLayout: host.onLayout, onScroll: host.onScroll, refreshControl: onRefresh === undefined ? undefined : (_jsx(RefreshControl, { colors: [theme.primaryText], onRefresh: () => void refresh(), progressBackgroundColor: theme.surface, refreshing: isRefreshing, tintColor: theme.primary })), renderItem: ({ index, item, section }) => (_jsx(PsInfiniteListItem, { count: section.data.length, index: index, children: renderItem(item) })), renderSectionFooter: ({ section }) => (_jsx(View, { style: {
35
+ gap: PsLayoutConfig.listSectionGap,
36
+ paddingBottom: section === shown[shown.length - 1] ? 0 : PsLayoutConfig.listGap,
37
+ paddingTop: section.footer === undefined ? 0 : PsLayoutConfig.listSectionGap,
38
+ }, children: section.footer === undefined ? null : (_jsx(PsText, { tone: 'muted', variant: 'caption', children: section.footer })) })), renderSectionHeader: ({ section }) => section.title === undefined ? null : (_jsx(View, { style: { paddingBottom: PsLayoutConfig.listSectionGap }, children: _jsx(PsText, { accessibilityRole: 'header', tone: 'muted', variant: 'label', children: section.title }) })), scrollEnabled: !host.isLocked, scrollEventThrottle: PsLayoutConfig.scrollEventThrottle, sections: shown, showsVerticalScrollIndicator: false, stickySectionHeadersEnabled: false, style: [{ backgroundColor: theme.background, flex: 1 }, style], testID: 'ps-infinite-list', windowSize: PsInfiniteConfig.windowSize }) }), header === undefined ? null : (_jsx(View, { pointerEvents: 'none', style: [PsScreenService.statusBarScrim(topInset), { backgroundColor: theme.background }], testID: 'ps-infinite-list-status-bar-scrim' }))] }));
39
+ };
@@ -0,0 +1,6 @@
1
+ export declare const PsInfiniteConfig: {
2
+ readonly endReachedThreshold: 1.5;
3
+ readonly initialNumToRender: 12;
4
+ readonly maxToRenderPerBatch: 12;
5
+ readonly windowSize: 9;
6
+ };
@@ -0,0 +1,6 @@
1
+ export const PsInfiniteConfig = {
2
+ endReachedThreshold: 1.5,
3
+ initialNumToRender: 12,
4
+ maxToRenderPerBatch: 12,
5
+ windowSize: 9,
6
+ };
@@ -0,0 +1,3 @@
1
+ import type { PsInfiniteDataOptions } from '../interfaces/ps-infinite-data-options';
2
+ import type { PsInfiniteDataResult } from '../interfaces/ps-infinite-data-result';
3
+ export declare const usePsInfiniteData: <T>({ getKey, isEnabled, key, loadPage, onSettled, onWriteFailed, refreshKey, }: PsInfiniteDataOptions<T>) => PsInfiniteDataResult<T>;
@@ -0,0 +1,192 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { usePsChangeQueue, usePsOptimisticRows } from '@pallasoft/ps-utilities/react';
3
+ import { PsInfiniteDataService } from '../services/ps-infinite-data-service';
4
+ export const usePsInfiniteData = ({ getKey, isEnabled = true, key, loadPage, onSettled, onWriteFailed, refreshKey, }) => {
5
+ const [state, setState] = useState(() => PsInfiniteDataService.initial());
6
+ const [isSaving, setIsSaving] = useState(false);
7
+ const isSavingRef = useRef(false);
8
+ const submittedRef = useRef(0);
9
+ const stateRef = useRef(state);
10
+ const committedRef = useRef([]);
11
+ const versionRef = useRef(0);
12
+ const latestRef = useRef({ getKey, loadPage, onSettled, onWriteFailed });
13
+ useEffect(() => {
14
+ latestRef.current = { getKey, loadPage, onSettled, onWriteFailed };
15
+ });
16
+ const commit = useCallback((next) => {
17
+ stateRef.current = next(stateRef.current);
18
+ setState(stateRef.current);
19
+ }, []);
20
+ const setSaving = useCallback((value) => {
21
+ isSavingRef.current = value;
22
+ setIsSaving(value);
23
+ }, []);
24
+ const fetchFirst = useCallback(async (isSilent) => {
25
+ versionRef.current += 1;
26
+ const version = versionRef.current;
27
+ if (!isSilent)
28
+ commit(() => PsInfiniteDataService.initial());
29
+ try {
30
+ const page = await latestRef.current.loadPage(null);
31
+ if (version !== versionRef.current)
32
+ return;
33
+ commit(() => ({
34
+ error: false,
35
+ isLoading: false,
36
+ isLoadingMore: false,
37
+ items: page.items,
38
+ nextCursor: page.nextCursor,
39
+ pages: 1,
40
+ }));
41
+ }
42
+ catch {
43
+ if (version !== versionRef.current)
44
+ return;
45
+ commit((current) => ({ ...current, error: true, isLoading: false, isLoadingMore: false }));
46
+ }
47
+ }, [commit]);
48
+ const reloadLoaded = useCallback(async () => {
49
+ versionRef.current += 1;
50
+ const version = versionRef.current;
51
+ const target = Math.max(stateRef.current.pages, 1);
52
+ try {
53
+ let items = [];
54
+ let cursor = null;
55
+ let pages = 0;
56
+ while (pages < target) {
57
+ const page = await latestRef.current.loadPage(cursor);
58
+ if (version !== versionRef.current)
59
+ return;
60
+ items = PsInfiniteDataService.merge(items, page.items, latestRef.current.getKey);
61
+ cursor = page.nextCursor;
62
+ pages += 1;
63
+ if (cursor === null)
64
+ break;
65
+ }
66
+ commit(() => ({
67
+ error: false,
68
+ isLoading: false,
69
+ isLoadingMore: false,
70
+ items,
71
+ nextCursor: cursor,
72
+ pages,
73
+ }));
74
+ }
75
+ catch {
76
+ if (version !== versionRef.current)
77
+ return;
78
+ commit((current) => ({ ...current, error: true }));
79
+ }
80
+ }, [commit]);
81
+ useEffect(() => {
82
+ committedRef.current = [];
83
+ setSaving(false);
84
+ if (!isEnabled) {
85
+ versionRef.current += 1;
86
+ commit(() => PsInfiniteDataService.initial());
87
+ return undefined;
88
+ }
89
+ void fetchFirst(false);
90
+ return () => {
91
+ versionRef.current += 1;
92
+ };
93
+ }, [key, isEnabled, fetchFirst, commit, setSaving]);
94
+ const refreshRef = useRef(refreshKey);
95
+ useEffect(() => {
96
+ if (refreshRef.current === refreshKey)
97
+ return;
98
+ refreshRef.current = refreshKey;
99
+ if (isEnabled && !isSavingRef.current && !stateRef.current.isLoading)
100
+ void reloadLoaded();
101
+ }, [isEnabled, refreshKey, reloadLoaded]);
102
+ const loadMore = useCallback(async () => {
103
+ const current = stateRef.current;
104
+ if (current.isLoading || current.isLoadingMore || current.nextCursor === null)
105
+ return;
106
+ const version = versionRef.current;
107
+ const cursor = current.nextCursor;
108
+ commit((value) => ({ ...value, isLoadingMore: true }));
109
+ try {
110
+ const page = await latestRef.current.loadPage(cursor);
111
+ if (version !== versionRef.current)
112
+ return;
113
+ commit((value) => ({
114
+ ...value,
115
+ error: false,
116
+ isLoadingMore: false,
117
+ items: PsInfiniteDataService.merge(value.items, page.items, latestRef.current.getKey),
118
+ nextCursor: page.nextCursor,
119
+ pages: value.pages + 1,
120
+ }));
121
+ }
122
+ catch {
123
+ if (version !== versionRef.current)
124
+ return;
125
+ commit((value) => ({ ...value, error: true, isLoadingMore: false }));
126
+ }
127
+ }, [commit]);
128
+ const refresh = useCallback(() => fetchFirst(true), [fetchFirst]);
129
+ const apply = useCallback((rows, updates) => PsInfiniteDataService.apply(rows, updates), []);
130
+ const optimistic = usePsOptimisticRows({
131
+ apply,
132
+ isSaving,
133
+ rows: state.items,
134
+ });
135
+ const rememberRef = useRef(optimistic.remember);
136
+ useEffect(() => {
137
+ rememberRef.current = optimistic.remember;
138
+ });
139
+ const send = useCallback(async (writes) => {
140
+ for (const item of writes) {
141
+ await item.request();
142
+ committedRef.current = [...committedRef.current, item];
143
+ item.resolve(true);
144
+ }
145
+ }, []);
146
+ const onFailed = useCallback((failed, dropped) => {
147
+ for (const item of [...failed, ...dropped]) {
148
+ optimistic.forgetLast();
149
+ item.resolve(false);
150
+ }
151
+ if (latestRef.current.onWriteFailed !== undefined)
152
+ latestRef.current.onWriteFailed();
153
+ }, [optimistic.forgetLast]);
154
+ const onDrained = useCallback(() => {
155
+ const through = submittedRef.current;
156
+ const settled = committedRef.current;
157
+ committedRef.current = [];
158
+ void (async () => {
159
+ const settle = latestRef.current.onSettled;
160
+ if (settle !== undefined)
161
+ await settle().catch(() => undefined);
162
+ if (settled.length > 0)
163
+ commit((current) => ({
164
+ ...current,
165
+ items: PsInfiniteDataService.apply(current.items, settled.map((item) => item.update)),
166
+ }));
167
+ if (submittedRef.current === through)
168
+ setSaving(false);
169
+ })();
170
+ }, [commit, setSaving]);
171
+ const queue = usePsChangeQueue({ onDrained, onFailed, send });
172
+ const { enqueue } = queue;
173
+ const write = useCallback((update, request) => {
174
+ if (stateRef.current.isLoading)
175
+ return Promise.resolve(false);
176
+ submittedRef.current += 1;
177
+ setSaving(true);
178
+ rememberRef.current(update);
179
+ return new Promise((resolve) => enqueue({ request, resolve, update }));
180
+ }, [enqueue, setSaving]);
181
+ return useMemo(() => ({
182
+ committedItems: state.items,
183
+ error: state.error,
184
+ hasMore: PsInfiniteDataService.hasMore(state),
185
+ isLoading: state.isLoading,
186
+ isLoadingMore: state.isLoadingMore,
187
+ items: optimistic.rows,
188
+ loadMore,
189
+ refresh,
190
+ write,
191
+ }), [loadMore, optimistic.rows, refresh, state, write]);
192
+ };
package/dist/index.d.ts CHANGED
@@ -174,3 +174,9 @@ export * from './interfaces/ps-barcode-scan';
174
174
  export * from './interfaces/ps-barcode-scanner-labels';
175
175
  export * from './interfaces/use-ps-barcode-scanner-result';
176
176
  export { PsBarcodeScannerService } from './services/ps-barcode-scanner-service';
177
+ export * from './components/ps-infinite-list';
178
+ export * from './hooks/use-ps-infinite-data';
179
+ export * from './interfaces/ps-infinite-data-options';
180
+ export * from './interfaces/ps-infinite-data-result';
181
+ export * from './interfaces/ps-infinite-data-update';
182
+ export * from './interfaces/ps-infinite-page';
package/dist/index.js CHANGED
@@ -169,3 +169,9 @@ export * from './interfaces/ps-barcode-scan';
169
169
  export * from './interfaces/ps-barcode-scanner-labels';
170
170
  export * from './interfaces/use-ps-barcode-scanner-result';
171
171
  export { PsBarcodeScannerService } from './services/ps-barcode-scanner-service';
172
+ export * from './components/ps-infinite-list';
173
+ export * from './hooks/use-ps-infinite-data';
174
+ export * from './interfaces/ps-infinite-data-options';
175
+ export * from './interfaces/ps-infinite-data-result';
176
+ export * from './interfaces/ps-infinite-data-update';
177
+ export * from './interfaces/ps-infinite-page';
@@ -0,0 +1,10 @@
1
+ import type { PsInfinitePage } from './ps-infinite-page';
2
+ export interface PsInfiniteDataOptions<T> {
3
+ getKey: (item: T) => string;
4
+ isEnabled?: boolean;
5
+ key: string;
6
+ loadPage: (cursor: string | null) => Promise<PsInfinitePage<T>>;
7
+ onSettled?: () => Promise<unknown>;
8
+ onWriteFailed?: () => void;
9
+ refreshKey?: number | string;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { PsInfiniteDataUpdate } from './ps-infinite-data-update';
2
+ export interface PsInfiniteDataResult<T> {
3
+ committedItems: T[];
4
+ error: boolean;
5
+ hasMore: boolean;
6
+ isLoading: boolean;
7
+ isLoadingMore: boolean;
8
+ items: T[];
9
+ loadMore: () => Promise<void>;
10
+ refresh: () => Promise<void>;
11
+ write: (update: PsInfiniteDataUpdate<T>, request: () => Promise<unknown>) => Promise<boolean>;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export interface PsInfiniteDataState<T> {
2
+ error: boolean;
3
+ isLoading: boolean;
4
+ isLoadingMore: boolean;
5
+ items: T[];
6
+ nextCursor: string | null;
7
+ pages: number;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type PsInfiniteDataUpdate<T> = (items: T[]) => T[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { PsInfiniteDataUpdate } from './ps-infinite-data-update';
2
+ export interface PsInfiniteDataWrite<T> {
3
+ request: () => Promise<unknown>;
4
+ resolve: (isSaved: boolean) => void;
5
+ update: PsInfiniteDataUpdate<T>;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export interface PsInfinitePage<T> {
2
+ items: T[];
3
+ nextCursor: string | null;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { PsInfiniteDataState } from '../interfaces/ps-infinite-data-state';
2
+ import type { PsInfiniteDataUpdate } from '../interfaces/ps-infinite-data-update';
3
+ export declare class PsInfiniteDataService {
4
+ static initial<T>(): PsInfiniteDataState<T>;
5
+ static merge<T>(existing: T[], incoming: T[], getKey: (item: T) => string): T[];
6
+ static apply<T>(rows: T[], updates: PsInfiniteDataUpdate<T>[]): T[];
7
+ static hasMore<T>(state: PsInfiniteDataState<T>): boolean;
8
+ }
@@ -0,0 +1,22 @@
1
+ export class PsInfiniteDataService {
2
+ static initial() {
3
+ return {
4
+ error: false,
5
+ isLoading: true,
6
+ isLoadingMore: false,
7
+ items: [],
8
+ nextCursor: null,
9
+ pages: 0,
10
+ };
11
+ }
12
+ static merge(existing, incoming, getKey) {
13
+ const keys = new Set(existing.map(getKey));
14
+ return [...existing, ...incoming.filter((item) => !keys.has(getKey(item)))];
15
+ }
16
+ static apply(rows, updates) {
17
+ return updates.reduce((current, update) => update(current), rows);
18
+ }
19
+ static hasMore(state) {
20
+ return state.nextCursor !== null;
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ import type { ViewStyle } from 'react-native';
2
+ import type { PsInfiniteSection } from '../components/ps-infinite-list/ps-infinite-list-props';
3
+ import type { PsMobileTheme } from '../interfaces/ps-mobile-theme';
4
+ export declare class PsInfiniteListService {
5
+ static readonly itemsKey = "ps-infinite-items";
6
+ static sections<T>(items: T[] | undefined, sections: PsInfiniteSection<T>[] | undefined): PsInfiniteSection<T>[];
7
+ static shouldLoadMore(hasMore: boolean, isLoading: boolean, isLoadingMore: boolean): boolean;
8
+ static itemFrame(theme: PsMobileTheme, index: number, count: number): ViewStyle;
9
+ static divider(theme: PsMobileTheme): ViewStyle;
10
+ }
@@ -0,0 +1,36 @@
1
+ import { PsListConfig } from '../constants/ps-list-config';
2
+ import { PsMobileControlConfig } from '../constants/ps-mobile-control-config';
3
+ export class PsInfiniteListService {
4
+ static sections(items, sections) {
5
+ if (sections !== undefined)
6
+ return sections.filter((section) => section.data.length > 0);
7
+ return items === undefined || items.length === 0
8
+ ? []
9
+ : [{ data: items, key: PsInfiniteListService.itemsKey }];
10
+ }
11
+ static shouldLoadMore(hasMore, isLoading, isLoadingMore) {
12
+ return hasMore && !isLoading && !isLoadingMore;
13
+ }
14
+ static itemFrame(theme, index, count) {
15
+ const isFirst = index === 0;
16
+ const isLast = index === count - 1;
17
+ const radius = PsMobileControlConfig.cardRadius;
18
+ return {
19
+ backgroundColor: theme.surface,
20
+ borderBottomLeftRadius: isLast ? radius : 0,
21
+ borderBottomRightRadius: isLast ? radius : 0,
22
+ borderBottomWidth: isLast ? 1 : 0,
23
+ borderColor: theme.line,
24
+ borderLeftWidth: 1,
25
+ borderRightWidth: 1,
26
+ borderTopLeftRadius: isFirst ? radius : 0,
27
+ borderTopRightRadius: isFirst ? radius : 0,
28
+ borderTopWidth: isFirst ? 1 : 0,
29
+ overflow: 'hidden',
30
+ };
31
+ }
32
+ static divider(theme) {
33
+ return { backgroundColor: theme.line, height: 1, marginLeft: PsListConfig.dividerInset };
34
+ }
35
+ }
36
+ PsInfiniteListService.itemsKey = 'ps-infinite-items';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-mobile-components",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "description": "Native iOS (SwiftUI) and Android (Jetpack Compose) Ps components for Pallasoft mobile apps",
6
6
  "main": "./dist/index.js",