@pallasoft/ps-mobile-components 1.4.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.
- package/dist/components/ps-infinite-list/index.d.ts +2 -0
- package/dist/components/ps-infinite-list/index.js +2 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-footer.d.ts +2 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-footer.js +5 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-item.d.ts +2 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-item.js +8 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-props.d.ts +34 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-props.js +1 -0
- package/dist/components/ps-infinite-list/ps-infinite-list.d.ts +2 -0
- package/dist/components/ps-infinite-list/ps-infinite-list.js +39 -0
- package/dist/components/ps-list/ps-list-action-row.d.ts +1 -1
- package/dist/components/ps-list/ps-list-action-row.js +4 -3
- package/dist/components/ps-list/ps-list-link-row-props.d.ts +1 -0
- package/dist/components/ps-list/ps-list-link-row.d.ts +1 -1
- package/dist/components/ps-list/ps-list-link-row.js +3 -3
- package/dist/components/ps-list/ps-list-props.d.ts +1 -0
- package/dist/constants/ps-infinite-config.d.ts +6 -0
- package/dist/constants/ps-infinite-config.js +6 -0
- package/dist/constants/ps-mobile-icon-key.d.ts +1 -0
- package/dist/constants/ps-sf-symbol-config.d.ts +1 -0
- package/dist/constants/ps-sf-symbol-config.js +1 -0
- package/dist/hooks/use-ps-infinite-data.d.ts +3 -0
- package/dist/hooks/use-ps-infinite-data.js +192 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/interfaces/ps-device-file.d.ts +1 -0
- package/dist/interfaces/ps-file-opener.d.ts +4 -0
- package/dist/interfaces/ps-file-opener.js +1 -0
- package/dist/interfaces/ps-infinite-data-options.d.ts +10 -0
- package/dist/interfaces/ps-infinite-data-options.js +1 -0
- package/dist/interfaces/ps-infinite-data-result.d.ts +12 -0
- package/dist/interfaces/ps-infinite-data-result.js +1 -0
- package/dist/interfaces/ps-infinite-data-state.d.ts +8 -0
- package/dist/interfaces/ps-infinite-data-state.js +1 -0
- package/dist/interfaces/ps-infinite-data-update.d.ts +1 -0
- package/dist/interfaces/ps-infinite-data-update.js +1 -0
- package/dist/interfaces/ps-infinite-data-write.d.ts +6 -0
- package/dist/interfaces/ps-infinite-data-write.js +1 -0
- package/dist/interfaces/ps-infinite-page.d.ts +4 -0
- package/dist/interfaces/ps-infinite-page.js +1 -0
- package/dist/services/ps-capture-picker-service.d.ts +1 -0
- package/dist/services/ps-capture-picker-service.js +5 -0
- package/dist/services/ps-device-file-bytes-service.d.ts +6 -0
- package/dist/services/ps-device-file-bytes-service.js +19 -0
- package/dist/services/ps-infinite-data-service.d.ts +8 -0
- package/dist/services/ps-infinite-data-service.js +22 -0
- package/dist/services/ps-infinite-list-service.d.ts +10 -0
- package/dist/services/ps-infinite-list-service.js +36 -0
- package/package.json +2 -2
|
@@ -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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { PsListActionRowProps } from './ps-list-props';
|
|
2
|
-
export declare const PsListActionRow: ({ children, iconKey, isDisabled, onPress, tone, }: PsListActionRowProps) => import("react").JSX.Element;
|
|
2
|
+
export declare const PsListActionRow: ({ children, iconKey, isDisabled, isExternal, onPress, tone, }: PsListActionRowProps) => import("react").JSX.Element;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsIconKey } from '@pallasoft/ps-icons';
|
|
2
3
|
import { Pressable } from 'react-native';
|
|
3
4
|
import { PsLayoutConfig } from '../../constants/ps-layout-config';
|
|
4
5
|
import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
|
|
@@ -8,11 +9,11 @@ import { PsRippleService } from '../../services/ps-ripple-service';
|
|
|
8
9
|
import { PsStateService } from '../../services/ps-state-service';
|
|
9
10
|
import { PsIcon } from '../ps-icon/ps-icon';
|
|
10
11
|
import { PsText } from '../ps-text/ps-text';
|
|
11
|
-
export const PsListActionRow = ({ children, iconKey, isDisabled = false, onPress, tone = 'primary', }) => {
|
|
12
|
+
export const PsListActionRow = ({ children, iconKey, isDisabled = false, isExternal = false, onPress, tone = 'primary', }) => {
|
|
12
13
|
const theme = usePsMobileTheme();
|
|
13
14
|
const isDestructive = tone === 'destructive';
|
|
14
15
|
const color = isDestructive ? theme.danger : theme.ink;
|
|
15
|
-
return (_jsxs(Pressable, { android_ripple: PsRippleService.resolve(theme, isDestructive ? PsRippleKind.Danger : PsRippleKind.Surface), accessibilityRole: 'button', accessibilityState: { disabled: isDisabled }, disabled: isDisabled, onPress: onPress, style: ({ pressed }) => ({
|
|
16
|
+
return (_jsxs(Pressable, { android_ripple: PsRippleService.resolve(theme, isDestructive ? PsRippleKind.Danger : PsRippleKind.Surface), accessibilityRole: isExternal ? 'link' : 'button', accessibilityState: { disabled: isDisabled }, disabled: isDisabled, onPress: onPress, style: ({ pressed }) => ({
|
|
16
17
|
alignItems: 'center',
|
|
17
18
|
backgroundColor: pressed ? PsStateService.pressedFill(theme, theme.surface) : theme.surface,
|
|
18
19
|
flexDirection: 'row',
|
|
@@ -20,5 +21,5 @@ export const PsListActionRow = ({ children, iconKey, isDisabled = false, onPress
|
|
|
20
21
|
minHeight: PsLayoutConfig.rowMinHeight,
|
|
21
22
|
opacity: isDisabled ? PsStateService.disabledOpacity() : 1,
|
|
22
23
|
paddingHorizontal: PsLayoutConfig.gutter,
|
|
23
|
-
}), testID: 'ps-list-action-row', children: [iconKey === undefined ? null : _jsx(PsIcon, { color: color, iconKey: iconKey }), _jsx(PsText, { style: { color }, weight: 'semibold', children: children })] }));
|
|
24
|
+
}), testID: 'ps-list-action-row', children: [iconKey === undefined ? null : _jsx(PsIcon, { color: color, iconKey: iconKey }), _jsx(PsText, { style: { color, flex: 1 }, weight: 'semibold', children: children }), isExternal ? (_jsx(PsIcon, { color: theme.muted, iconKey: PsIconKey.ExternalLink, size: PsMobileControlConfig.iconSizeInline })) : null] }));
|
|
24
25
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { PsListLinkRowProps } from './ps-list-link-row-props';
|
|
2
|
-
export declare const PsListLinkRow: ({ count, countLabel, iconKey, isAlert, isLoading, onPress, subtitle, title, }: PsListLinkRowProps) => import("react").JSX.Element;
|
|
2
|
+
export declare const PsListLinkRow: ({ count, countLabel, iconKey, isAlert, isExternal, isLoading, onPress, subtitle, title, }: PsListLinkRowProps) => import("react").JSX.Element;
|
|
@@ -12,13 +12,13 @@ import { PsCountBadge } from '../ps-count-badge/ps-count-badge';
|
|
|
12
12
|
import { PsIcon } from '../ps-icon/ps-icon';
|
|
13
13
|
import { PsText } from '../ps-text/ps-text';
|
|
14
14
|
import { PsListLinkRowSkeleton } from './ps-list-link-row-skeleton';
|
|
15
|
-
export const PsListLinkRow = ({ count, countLabel, iconKey, isAlert = false, isLoading = false, onPress, subtitle, title, }) => {
|
|
15
|
+
export const PsListLinkRow = ({ count, countLabel, iconKey, isAlert = false, isExternal = false, isLoading = false, onPress, subtitle, title, }) => {
|
|
16
16
|
const theme = usePsMobileTheme();
|
|
17
17
|
const haptic = usePsHaptics();
|
|
18
18
|
if (isLoading)
|
|
19
19
|
return _jsx(PsListLinkRowSkeleton, {});
|
|
20
20
|
const hasCount = count !== undefined && count > 0;
|
|
21
|
-
return (_jsxs(Pressable, { android_ripple: PsRippleService.resolve(theme, PsRippleKind.Surface), accessibilityHint: countLabel, accessibilityRole: 'button', onPress: () => {
|
|
21
|
+
return (_jsxs(Pressable, { android_ripple: PsRippleService.resolve(theme, PsRippleKind.Surface), accessibilityHint: countLabel, accessibilityRole: isExternal ? 'link' : 'button', onPress: () => {
|
|
22
22
|
haptic('light');
|
|
23
23
|
onPress();
|
|
24
24
|
}, style: ({ pressed }) => ({
|
|
@@ -36,5 +36,5 @@ export const PsListLinkRow = ({ count, countLabel, iconKey, isAlert = false, isL
|
|
|
36
36
|
height: PsLayoutConfig.linkRowIconBox,
|
|
37
37
|
justifyContent: 'center',
|
|
38
38
|
width: PsLayoutConfig.linkRowIconBox,
|
|
39
|
-
}, children: _jsx(PsIcon, { color: theme.primaryText, iconKey: iconKey, size: PsLayoutConfig.linkRowIconSize }) }), _jsxs(View, { style: { flex: 1, gap: PsLayoutConfig.rowLineGap }, children: [_jsx(PsText, { numberOfLines: 1, weight: 'semibold', children: title }), subtitle === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: 'muted', variant: 'small', children: subtitle }))] }), hasCount ? _jsx(PsCountBadge, { count: count, isAlert: isAlert, label: countLabel }) : null, _jsx(PsIcon, { color: theme.muted, iconKey: PsIconKey.ChevronRight, size: PsMobileControlConfig.iconSizeInline })] }));
|
|
39
|
+
}, children: _jsx(PsIcon, { color: theme.primaryText, iconKey: iconKey, size: PsLayoutConfig.linkRowIconSize }) }), _jsxs(View, { style: { flex: 1, gap: PsLayoutConfig.rowLineGap }, children: [_jsx(PsText, { numberOfLines: 1, weight: 'semibold', children: title }), subtitle === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: 'muted', variant: 'small', children: subtitle }))] }), hasCount ? _jsx(PsCountBadge, { count: count, isAlert: isAlert, label: countLabel }) : null, _jsx(PsIcon, { color: theme.muted, iconKey: isExternal ? PsIconKey.ExternalLink : PsIconKey.ChevronRight, size: PsMobileControlConfig.iconSizeInline })] }));
|
|
40
40
|
};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type PsInfiniteDataUpdate<T> = (items: T[]) => T[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,6 +3,7 @@ import type { PsCaptureOptions } from '../interfaces/ps-capture-options';
|
|
|
3
3
|
import type { PsDeviceFile } from '../interfaces/ps-device-file';
|
|
4
4
|
export declare class PsCapturePickerService {
|
|
5
5
|
static pick(source: PsCaptureSource, options?: PsCaptureOptions): Promise<PsDeviceFile[]>;
|
|
6
|
+
private static pickRaw;
|
|
6
7
|
private static scan;
|
|
7
8
|
private static pickImage;
|
|
8
9
|
private static pickDocument;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { PsCaptureConfig } from '../constants/ps-capture-config';
|
|
2
2
|
import { PsCaptureSource } from '../constants/ps-capture-source';
|
|
3
|
+
import { PsDeviceFileBytesService } from './ps-device-file-bytes-service';
|
|
3
4
|
import { PsDeviceFileService } from './ps-device-file-service';
|
|
4
5
|
import { PsDocumentScannerService } from './ps-document-scanner-service';
|
|
5
6
|
export class PsCapturePickerService {
|
|
6
7
|
static async pick(source, options = {}) {
|
|
8
|
+
const files = await PsCapturePickerService.pickRaw(source, options);
|
|
9
|
+
return files.map((file) => PsDeviceFileBytesService.withBytes(file));
|
|
10
|
+
}
|
|
11
|
+
static async pickRaw(source, options) {
|
|
7
12
|
if (source === PsCaptureSource.Scan)
|
|
8
13
|
return PsCapturePickerService.scan(options);
|
|
9
14
|
const file = source === PsCaptureSource.Files
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PsDeviceFile } from '../interfaces/ps-device-file';
|
|
2
|
+
import type { PsFileOpener } from '../interfaces/ps-file-opener';
|
|
3
|
+
export declare class PsDeviceFileBytesService {
|
|
4
|
+
static withBytes(file: PsDeviceFile, open?: PsFileOpener | null): PsDeviceFile;
|
|
5
|
+
private static opener;
|
|
6
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class PsDeviceFileBytesService {
|
|
2
|
+
static withBytes(file, open = PsDeviceFileBytesService.opener()) {
|
|
3
|
+
if (open === null)
|
|
4
|
+
return file;
|
|
5
|
+
return {
|
|
6
|
+
...file,
|
|
7
|
+
bytes: async () => new Uint8Array(await open(file.uri).arrayBuffer()),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
static opener() {
|
|
11
|
+
try {
|
|
12
|
+
const loaded = require('expo-file-system');
|
|
13
|
+
return typeof loaded.File === 'function' ? (uri) => new loaded.File(uri) : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -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.
|
|
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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@pallasoft/ps-design-system": "^1.19.0",
|
|
38
|
-
"@pallasoft/ps-icons": "^1.
|
|
38
|
+
"@pallasoft/ps-icons": "^1.12.0",
|
|
39
39
|
"@pallasoft/ps-utilities": "^2.27.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|