@sincpro/mobile-ui 1.0.1 → 2.0.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/README.md +701 -231
- package/dist/Actions/Actions.Button.d.ts +15 -12
- package/dist/Actions/Actions.Button.js +25 -14
- package/dist/Actions/Actions.Dock.d.ts +20 -0
- package/dist/Actions/Actions.Dock.js +89 -0
- package/dist/Actions/Actions.FAB.d.ts +2 -1
- package/dist/Actions/Actions.FAB.js +5 -3
- package/dist/Actions/Actions.FilterButton.d.ts +10 -2
- package/dist/Actions/Actions.FilterButton.js +21 -6
- package/dist/Actions/Actions.IconButton.d.ts +4 -2
- package/dist/Actions/Actions.IconButton.js +7 -3
- package/dist/Actions/index.d.ts +6 -2
- package/dist/Actions/index.js +4 -1
- package/dist/Dialog/BottomSheet/BottomSheet.Root.js +22 -34
- package/dist/Dialog/Dialog.ActionSheet.d.ts +2 -1
- package/dist/Dialog/Dialog.ActionSheet.js +17 -21
- package/dist/Dialog/Dialog.Confirmation.js +8 -6
- package/dist/Dialog/Dialog.EditValue.js +3 -1
- package/dist/Dialog/Dialog.NativeSheet.d.ts +10 -0
- package/dist/Dialog/Dialog.NativeSheet.js +21 -0
- package/dist/Dialog/Dialog.Sheet.d.ts +5 -1
- package/dist/Dialog/Dialog.Sheet.js +30 -31
- package/dist/Display/Display.Accordion.js +7 -3
- package/dist/Display/Display.AmountSummary.d.ts +28 -0
- package/dist/Display/Display.AmountSummary.js +25 -0
- package/dist/Display/Display.Carousel.d.ts +27 -4
- package/dist/Display/Display.Carousel.js +112 -12
- package/dist/Display/Display.CopyableText.d.ts +9 -2
- package/dist/Display/Display.CopyableText.js +75 -7
- package/dist/Display/Display.ExpoImage.d.ts +8 -0
- package/dist/Display/Display.ExpoImage.js +15 -0
- package/dist/Display/Display.Icon.js +2 -1
- package/dist/Display/Display.Image.d.ts +10 -6
- package/dist/Display/Display.Image.js +39 -6
- package/dist/Display/Display.KeyValue.d.ts +11 -4
- package/dist/Display/Display.KeyValue.js +6 -4
- package/dist/Display/Display.ListGroup.d.ts +23 -0
- package/dist/Display/Display.ListGroup.js +17 -0
- package/dist/Display/Display.ListItem.d.ts +15 -2
- package/dist/Display/Display.ListItem.js +25 -8
- package/dist/Display/Display.Logo.d.ts +2 -2
- package/dist/Display/Display.Logo.js +3 -2
- package/dist/Display/Display.MediaCard.d.ts +2 -2
- package/dist/Display/Display.MediaEmbed.d.ts +2 -2
- package/dist/Display/Display.ProductCard.d.ts +2 -2
- package/dist/Display/Display.SectionHeader.d.ts +11 -1
- package/dist/Display/Display.SectionHeader.js +11 -2
- package/dist/Display/Display.Story.d.ts +3 -3
- package/dist/Display/Display.Story.js +5 -4
- package/dist/Display/Display.StoryTray.d.ts +4 -4
- package/dist/Display/Display.StoryTray.js +3 -2
- package/dist/Display/Display.StoryViewer.d.ts +3 -3
- package/dist/Display/Display.StoryViewer.js +6 -3
- package/dist/Display/Display.SwipeableRow.d.ts +3 -1
- package/dist/Display/Display.SwipeableRow.js +3 -3
- package/dist/Display/index.d.ts +13 -0
- package/dist/Display/index.js +6 -0
- package/dist/Feedback/Feedback.Banner.js +1 -1
- package/dist/Feedback/Feedback.DomainSplashScreen.d.ts +13 -4
- package/dist/Feedback/Feedback.DomainSplashScreen.js +68 -53
- package/dist/Feedback/Feedback.EmptyState.d.ts +2 -2
- package/dist/Feedback/Feedback.EmptyState.js +3 -2
- package/dist/Feedback/Feedback.Error.d.ts +2 -1
- package/dist/Feedback/Feedback.Error.js +4 -3
- package/dist/Feedback/Feedback.Loading.d.ts +6 -2
- package/dist/Feedback/Feedback.Loading.js +2 -2
- package/dist/Feedback/Feedback.Progress.d.ts +6 -3
- package/dist/Feedback/Feedback.Progress.js +29 -19
- package/dist/Feedback/Feedback.ProgressCircle.d.ts +3 -2
- package/dist/Feedback/Feedback.ProgressCircle.js +10 -7
- package/dist/Feedback/Feedback.Skeleton.d.ts +4 -4
- package/dist/Feedback/Feedback.Skeleton.js +15 -3
- package/dist/Feedback/Feedback.Spinner.d.ts +3 -3
- package/dist/Feedback/Feedback.Spinner.js +40 -9
- package/dist/Feedback/Feedback.Splash.d.ts +2 -2
- package/dist/Feedback/Feedback.Splash.js +5 -3
- package/dist/Feedback/Toast.context.d.ts +64 -3
- package/dist/Feedback/Toast.context.js +112 -31
- package/dist/Feedback/index.d.ts +15 -1
- package/dist/Feedback/index.js +4 -1
- package/dist/Form/Form.Checkbox.d.ts +3 -1
- package/dist/Form/Form.Checkbox.js +2 -2
- package/dist/Form/Form.DatePicker.js +3 -2
- package/dist/Form/Form.Dropdown.d.ts +15 -3
- package/dist/Form/Form.Dropdown.js +21 -6
- package/dist/Form/Form.ErrorSummary.d.ts +23 -0
- package/dist/Form/Form.ErrorSummary.js +53 -0
- package/dist/Form/Form.FilterButton.d.ts +1 -1
- package/dist/Form/Form.Input.d.ts +4 -3
- package/dist/Form/Form.Input.js +2 -2
- package/dist/Form/Form.KeyboardForm.d.ts +11 -0
- package/dist/Form/Form.KeyboardForm.js +9 -0
- package/dist/Form/Form.KeyboardToolbar.d.ts +21 -0
- package/dist/Form/Form.KeyboardToolbar.js +70 -0
- package/dist/Form/Form.Select.d.ts +2 -1
- package/dist/Form/Form.Select.js +26 -26
- package/dist/Form/index.d.ts +9 -0
- package/dist/Form/index.js +6 -0
- package/dist/Navigation/Navigation.AppBar.js +8 -6
- package/dist/Navigation/Navigation.BottomNav.js +10 -5
- package/dist/Navigation/Navigation.Header.d.ts +2 -2
- package/dist/Navigation/Navigation.Menu.js +10 -3
- package/dist/Navigation/Navigation.RowItem.d.ts +1 -1
- package/dist/Navigation/Navigation.RowItem.js +2 -2
- package/dist/Navigation/Navigation.Search.d.ts +41 -0
- package/dist/Navigation/Navigation.Search.js +47 -0
- package/dist/Navigation/Navigation.SearchBar.d.ts +5 -1
- package/dist/Navigation/Navigation.SearchBar.js +25 -7
- package/dist/Navigation/Navigation.SearchSuggestions.d.ts +14 -0
- package/dist/Navigation/Navigation.SearchSuggestions.js +7 -0
- package/dist/Navigation/Navigation.SegmentedControl.d.ts +11 -1
- package/dist/Navigation/Navigation.SegmentedControl.js +29 -15
- package/dist/Navigation/index.d.ts +13 -0
- package/dist/Navigation/index.js +4 -0
- package/dist/Typography/Typography.Heading.js +1 -1
- package/dist/Typography/Typography.SelectableText.d.ts +10 -0
- package/dist/Typography/Typography.SelectableText.js +16 -0
- package/dist/Typography/Typography.Text.js +4 -20
- package/dist/Typography/index.d.ts +3 -0
- package/dist/Typography/index.js +3 -0
- package/dist/branding.d.ts +3 -2
- package/dist/layouts/AuthContainer.d.ts +4 -1
- package/dist/layouts/AuthContainer.js +3 -3
- package/dist/primitives/BaseButton.d.ts +15 -12
- package/dist/primitives/BaseButton.js +21 -9
- package/dist/theme/typography.d.ts +16 -1
- package/dist/theme/typography.js +19 -0
- package/dist/utils/criteria.d.ts +56 -0
- package/dist/utils/criteria.js +99 -0
- package/dist/utils/image.d.ts +3 -0
- package/dist/utils/image.js +13 -0
- package/dist/utils/keys.d.ts +1 -0
- package/dist/utils/keys.js +7 -0
- package/dist/utils/progress.d.ts +1 -0
- package/dist/utils/progress.js +5 -0
- package/dist/utils/reorder.d.ts +4 -0
- package/dist/utils/reorder.js +29 -0
- package/dist/utils/useReducedMotion.d.ts +1 -0
- package/dist/utils/useReducedMotion.js +43 -0
- package/dist/utils/useSheetTranslation.d.ts +5 -0
- package/dist/utils/useSheetTranslation.js +23 -0
- package/dist/views/AppSplashView.d.ts +3 -2
- package/dist/views/AppSplashView.js +3 -2
- package/dist/views/AuthFormView.d.ts +6 -3
- package/dist/views/AuthFormView.js +2 -2
- package/dist/views/FormViewV2.d.ts +9 -4
- package/dist/views/FormViewV2.js +35 -16
- package/dist/views/ListViewV2.context.d.ts +3 -1
- package/dist/views/ListViewV2.context.js +41 -21
- package/dist/views/ListViewV2.d.ts +18 -7
- package/dist/views/ListViewV2.js +69 -35
- package/dist/views/types/IListView.js +6 -5
- package/dist/widgets/CollectionOptions.d.ts +15 -0
- package/dist/widgets/CollectionOptions.js +129 -0
- package/dist/widgets/FilterBar.d.ts +21 -0
- package/dist/widgets/FilterBar.js +14 -0
- package/dist/widgets/FilterBuilder.d.ts +10 -0
- package/dist/widgets/FilterBuilder.js +115 -0
- package/dist/widgets/HomeHeader.d.ts +2 -2
- package/dist/widgets/PromoModal.js +3 -1
- package/dist/widgets/QueryChoice.d.ts +10 -0
- package/dist/widgets/QueryChoice.js +13 -0
- package/dist/widgets/ReorderableList.d.ts +51 -0
- package/dist/widgets/ReorderableList.js +138 -0
- package/dist/widgets/ScreenHeader.d.ts +2 -2
- package/dist/widgets/index.d.ts +4 -0
- package/dist/widgets/index.js +3 -0
- package/package.json +18 -1
package/dist/views/FormViewV2.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "nativewind/jsx-runtime";
|
|
2
2
|
import { Display } from "../Display/index.js";
|
|
3
3
|
import { ErrorBoundary, Feedback } from "../Feedback/index.js";
|
|
4
|
+
import KeyboardForm from "../Form/Form.KeyboardForm.js";
|
|
4
5
|
import { useBottomInset } from "../layouts/BottomInset.js";
|
|
5
6
|
import Container from "../layouts/Container.js";
|
|
6
7
|
import { AppBar } from "../Navigation/Navigation.AppBar.js";
|
|
@@ -10,13 +11,31 @@ import { useTheme } from "../theme/index.js";
|
|
|
10
11
|
import { FormViewProvider, useFormView } from "./FormViewV2.context.js";
|
|
11
12
|
import React from "react";
|
|
12
13
|
import { RefreshControl, View } from "react-native";
|
|
13
|
-
|
|
14
|
-
function
|
|
14
|
+
const RefreshErrorContext = React.createContext(undefined);
|
|
15
|
+
export async function refreshFormData(onRefresh, onRefreshError, setShowRefreshing) {
|
|
16
|
+
setShowRefreshing(true);
|
|
17
|
+
try {
|
|
18
|
+
await onRefresh?.();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error("[FormViewV2] Refresh failed", error);
|
|
22
|
+
try {
|
|
23
|
+
onRefreshError?.(error);
|
|
24
|
+
}
|
|
25
|
+
catch (callbackError) {
|
|
26
|
+
console.error("[FormViewV2] onRefreshError callback failed", callbackError);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
setShowRefreshing(false);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function FormViewRoot({ name, description, item, isLoading = false, isEmpty = false, hasError = false, errorMessage, readonly = false, onRefresh, onRefreshError, onRetry, onBack, withContainer = true, children, }) {
|
|
15
34
|
const Wrapper = withContainer ? Container : View;
|
|
16
35
|
const wrapperProps = withContainer
|
|
17
36
|
? { edges: ["bottom", "left", "right"] }
|
|
18
37
|
: { style: { flex: 1 } };
|
|
19
|
-
return (_jsx(FormViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, isEmpty: isEmpty, isLoading: isLoading, item: item, name: name, onBack: onBack, onRefresh: onRefresh, onRetry: onRetry, readonly: readonly, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }));
|
|
38
|
+
return (_jsx(FormViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, isEmpty: isEmpty, isLoading: isLoading, item: item, name: name, onBack: onBack, onRefresh: onRefresh, onRetry: onRetry, readonly: readonly, children: _jsx(RefreshErrorContext.Provider, { value: onRefreshError, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }) }));
|
|
20
39
|
}
|
|
21
40
|
function Header({ variant = "default", tone, actions, children, topSpacing, background, }) {
|
|
22
41
|
const { name, description, onBack } = useFormView();
|
|
@@ -36,24 +55,24 @@ function HeaderActionsBar({ children }) {
|
|
|
36
55
|
function HeaderSegmented(props) {
|
|
37
56
|
return _jsx(SegmentedControl, { ...props });
|
|
38
57
|
}
|
|
39
|
-
function Content({ withMargin = false, children, }) {
|
|
58
|
+
function Content({ withMargin = false, keyboardToolbar = false, keyboardFormProps, children, }) {
|
|
40
59
|
const { name, isLoading, isEmpty, hasError, errorMessage, item, onRefresh, onRetry, onBack, showRefreshing, setShowRefreshing, } = useFormView();
|
|
41
60
|
const bottomInset = useBottomInset();
|
|
42
61
|
const theme = useTheme();
|
|
62
|
+
const onRefreshError = React.useContext(RefreshErrorContext);
|
|
43
63
|
const handleRefresh = React.useCallback(async () => {
|
|
44
|
-
setShowRefreshing
|
|
45
|
-
|
|
46
|
-
await onRefresh();
|
|
47
|
-
}
|
|
48
|
-
setShowRefreshing(false);
|
|
49
|
-
}, [onRefresh, setShowRefreshing]);
|
|
64
|
+
await refreshFormData(onRefresh, onRefreshError, setShowRefreshing);
|
|
65
|
+
}, [onRefresh, onRefreshError, setShowRefreshing]);
|
|
50
66
|
const isCenteredContent = isLoading || isEmpty || !item || hasError;
|
|
51
|
-
return (_jsx(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
return (_jsx(KeyboardForm, { ...keyboardFormProps, contentContainerStyle: [
|
|
68
|
+
{
|
|
69
|
+
flexGrow: 1,
|
|
70
|
+
justifyContent: isCenteredContent ? "center" : "flex-start",
|
|
71
|
+
alignItems: isCenteredContent ? "center" : "stretch",
|
|
72
|
+
paddingBottom: bottomInset,
|
|
73
|
+
},
|
|
74
|
+
keyboardFormProps?.contentContainerStyle,
|
|
75
|
+
], keyboardShouldPersistTaps: keyboardFormProps?.keyboardShouldPersistTaps ?? "handled", refreshControl: keyboardFormProps?.refreshControl ?? (_jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing })), style: [{ flex: 1, backgroundColor: theme.bg.page }, keyboardFormProps?.style], toolbar: keyboardToolbar, children: isCenteredContent ? (_jsx(_Fragment, { children: isLoading ? (_jsx(Feedback.Loading, { message: `Cargando ${name}...` })) : hasError ? (_jsx(Feedback.Error, { message: errorMessage, onBack: onBack, onRetry: onRetry })) : (_jsx(Feedback.Empty, {})) })) : (_jsx(View, { style: withMargin ? { paddingHorizontal: 16 } : undefined, children: children })) }));
|
|
57
76
|
}
|
|
58
77
|
/**
|
|
59
78
|
* @deprecated Content already handles isEmpty/!item by rendering Feedback.Empty internally.
|
|
@@ -17,8 +17,9 @@ interface ListViewContextValue<T> {
|
|
|
17
17
|
onBack?: () => void;
|
|
18
18
|
onLoadMore?: () => void;
|
|
19
19
|
loadingMore?: boolean;
|
|
20
|
+
hasMore: boolean;
|
|
20
21
|
}
|
|
21
|
-
export declare function ListViewProvider<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, children, }: {
|
|
22
|
+
export declare function ListViewProvider<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, hasMore, children, }: {
|
|
22
23
|
name: string;
|
|
23
24
|
description?: string;
|
|
24
25
|
items: T[];
|
|
@@ -34,6 +35,7 @@ export declare function ListViewProvider<T>({ name, description, items, isLoadin
|
|
|
34
35
|
onBack?: () => void;
|
|
35
36
|
onLoadMore?: () => void;
|
|
36
37
|
loadingMore?: boolean;
|
|
38
|
+
hasMore?: boolean;
|
|
37
39
|
children: ReactNode;
|
|
38
40
|
}): import("react").JSX.Element;
|
|
39
41
|
export declare function useListView<T>(): ListViewContextValue<T>;
|
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
import { jsx as _jsx } from "nativewind/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useState } from "react";
|
|
2
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
3
3
|
const Context = createContext(null);
|
|
4
|
-
export function ListViewProvider({ name, description, items, isLoading = false, hasError = false, errorMessage, readonly = false, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore = false, children, }) {
|
|
4
|
+
export function ListViewProvider({ name, description, items, isLoading = false, hasError = false, errorMessage, readonly = false, onRefresh, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore = false, hasMore = true, children, }) {
|
|
5
5
|
const [showRefreshing, setShowRefreshing] = useState(false);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
6
|
+
const value = useMemo(() => ({
|
|
7
|
+
name,
|
|
8
|
+
description,
|
|
9
|
+
items,
|
|
10
|
+
isLoading,
|
|
11
|
+
hasError,
|
|
12
|
+
errorMessage,
|
|
13
|
+
readonly,
|
|
14
|
+
showRefreshing,
|
|
15
|
+
setShowRefreshing,
|
|
16
|
+
onRefresh,
|
|
17
|
+
onRetry,
|
|
18
|
+
onPressItem,
|
|
19
|
+
onDeleteItem,
|
|
20
|
+
onSearch,
|
|
21
|
+
onBack,
|
|
22
|
+
onLoadMore,
|
|
23
|
+
loadingMore,
|
|
24
|
+
hasMore,
|
|
25
|
+
}), [
|
|
26
|
+
name,
|
|
27
|
+
description,
|
|
28
|
+
items,
|
|
29
|
+
isLoading,
|
|
30
|
+
hasError,
|
|
31
|
+
errorMessage,
|
|
32
|
+
readonly,
|
|
33
|
+
showRefreshing,
|
|
34
|
+
onRefresh,
|
|
35
|
+
onRetry,
|
|
36
|
+
onPressItem,
|
|
37
|
+
onDeleteItem,
|
|
38
|
+
onSearch,
|
|
39
|
+
onBack,
|
|
40
|
+
onLoadMore,
|
|
41
|
+
loadingMore,
|
|
42
|
+
hasMore,
|
|
43
|
+
]);
|
|
44
|
+
return _jsx(Context.Provider, { value: value, children: children });
|
|
25
45
|
}
|
|
26
46
|
export function useListView() {
|
|
27
47
|
const context = useContext(Context);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type SearchHistoryProps } from "../Display/Display.SearchHistory.js";
|
|
2
2
|
import type { AppBarBackground } from "../Navigation/Navigation.AppBar.js";
|
|
3
3
|
import { type SegmentedControlProps } from "../Navigation/Navigation.SegmentedControl.js";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import { IRowItem } from "./types/IListView.js";
|
|
5
|
+
import React, { ReactNode, type Ref } from "react";
|
|
6
|
+
import { FlatList, type FlatListProps, type ScrollViewProps, StyleProp, ViewStyle } from "react-native";
|
|
6
7
|
interface ListViewProps<T> {
|
|
7
8
|
name: string;
|
|
8
9
|
description?: string;
|
|
@@ -12,6 +13,7 @@ interface ListViewProps<T> {
|
|
|
12
13
|
errorMessage?: string;
|
|
13
14
|
readonly?: boolean;
|
|
14
15
|
onRefresh?: () => Promise<void>;
|
|
16
|
+
onRefreshError?: (error: unknown) => void;
|
|
15
17
|
onRetry?: () => void;
|
|
16
18
|
onPressItem?: (item: T) => void;
|
|
17
19
|
onDeleteItem?: (item: T) => void;
|
|
@@ -21,11 +23,13 @@ interface ListViewProps<T> {
|
|
|
21
23
|
onLoadMore?: () => void;
|
|
22
24
|
/** Show a footer spinner while the next page loads. */
|
|
23
25
|
loadingMore?: boolean;
|
|
26
|
+
hasMore?: boolean;
|
|
24
27
|
backgroundColor?: string;
|
|
25
28
|
withContainer?: boolean;
|
|
26
29
|
children: ReactNode;
|
|
27
30
|
}
|
|
28
|
-
declare function
|
|
31
|
+
export declare function refreshListData(onRefresh: (() => Promise<void>) | undefined, onRefreshError: ((error: unknown) => void) | undefined, setShowRefreshing: (value: boolean) => void): Promise<void>;
|
|
32
|
+
declare function ListViewRoot<T>({ name, description, items, isLoading, hasError, errorMessage, readonly, onRefresh, onRefreshError, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore, hasMore, backgroundColor, withContainer, children, }: ListViewProps<T>): React.JSX.Element;
|
|
29
33
|
interface HeaderProps {
|
|
30
34
|
/** AppBar display variant. `large` = big title (list/index). `center` = modal/detail. */
|
|
31
35
|
variant?: "default" | "large" | "center";
|
|
@@ -79,14 +83,21 @@ declare function FilterChip({ label, active, onPress }: FilterChipProps): React.
|
|
|
79
83
|
declare function FilterChips({ children }: {
|
|
80
84
|
children: ReactNode;
|
|
81
85
|
}): React.JSX.Element;
|
|
82
|
-
|
|
86
|
+
type ListScrollProps = Pick<ScrollViewProps, "keyboardShouldPersistTaps" | "keyboardDismissMode" | "testID">;
|
|
87
|
+
export type ListViewVirtualizationProps = Pick<FlatListProps<IRowItem<unknown>>, "initialNumToRender" | "maxToRenderPerBatch" | "windowSize" | "updateCellsBatchingPeriod" | "removeClippedSubviews" | "extraData" | "getItemLayout" | "onViewableItemsChanged" | "viewabilityConfig" | "onEndReachedThreshold" | "maintainVisibleContentPosition">;
|
|
88
|
+
interface ListContentOptions {
|
|
89
|
+
listProps?: ListViewVirtualizationProps;
|
|
90
|
+
listRef?: Ref<FlatList<IRowItem<unknown>>>;
|
|
91
|
+
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
92
|
+
}
|
|
93
|
+
declare function Content({ children, withMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, listProps, listRef, contentContainerStyle, }: {
|
|
83
94
|
children?: (item: any) => ReactNode;
|
|
84
95
|
withMargin?: boolean;
|
|
85
|
-
}): React.JSX.Element;
|
|
86
|
-
declare function ContentList({ children, contentWithMargin, }: {
|
|
96
|
+
} & ListScrollProps & ListContentOptions): React.JSX.Element;
|
|
97
|
+
declare function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, listProps, listRef, contentContainerStyle, }: {
|
|
87
98
|
children?: (item: any) => ReactNode;
|
|
88
99
|
contentWithMargin: boolean;
|
|
89
|
-
}): React.JSX.Element;
|
|
100
|
+
} & ListScrollProps & ListContentOptions): React.JSX.Element;
|
|
90
101
|
declare function Footer({ children }: {
|
|
91
102
|
children: ReactNode;
|
|
92
103
|
}): React.JSX.Element | null;
|
package/dist/views/ListViewV2.js
CHANGED
|
@@ -11,15 +11,34 @@ import { cn, tv } from "../theme/tw.js";
|
|
|
11
11
|
import { Typography } from "../Typography/index.js";
|
|
12
12
|
import { ListViewProvider, useListView } from "./ListViewV2.context.js";
|
|
13
13
|
import { toRowItems } from "./types/IListView.js";
|
|
14
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
14
|
+
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
15
15
|
import { FlatList, RefreshControl, ScrollView, TouchableOpacity, View, } from "react-native";
|
|
16
|
-
|
|
16
|
+
const RefreshErrorContext = createContext(undefined);
|
|
17
|
+
export async function refreshListData(onRefresh, onRefreshError, setShowRefreshing) {
|
|
18
|
+
setShowRefreshing(true);
|
|
19
|
+
try {
|
|
20
|
+
await onRefresh?.();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error("[ListViewV2] Refresh failed", error);
|
|
24
|
+
try {
|
|
25
|
+
onRefreshError?.(error);
|
|
26
|
+
}
|
|
27
|
+
catch (callbackError) {
|
|
28
|
+
console.error("[ListViewV2] onRefreshError callback failed", callbackError);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
setShowRefreshing(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function ListViewRoot({ name, description, items, isLoading = false, hasError = false, errorMessage, readonly = false, onRefresh, onRefreshError, onRetry, onPressItem, onDeleteItem, onSearch, onBack, onLoadMore, loadingMore = false, hasMore = true, backgroundColor, withContainer = true, children, }) {
|
|
17
36
|
const Wrapper = withContainer ? Container : View;
|
|
18
37
|
const wrapperProps = withContainer
|
|
19
38
|
? { edges: ["bottom", "left", "right"] }
|
|
20
39
|
: { className: "flex-1 bg-bg-page" };
|
|
21
40
|
// Propagate contentWithMargin to ContentList via context
|
|
22
|
-
return (_jsx(ListViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, isLoading: isLoading, items: items, loadingMore: loadingMore, name: name, onBack: onBack, onDeleteItem: onDeleteItem, onLoadMore: onLoadMore, onPressItem: onPressItem, onRefresh: onRefresh, onRetry: onRetry, onSearch: onSearch, readonly: readonly, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }));
|
|
41
|
+
return (_jsx(ListViewProvider, { description: description, errorMessage: errorMessage, hasError: hasError, hasMore: hasMore, isLoading: isLoading, items: items, loadingMore: loadingMore, name: name, onBack: onBack, onDeleteItem: onDeleteItem, onLoadMore: onLoadMore, onPressItem: onPressItem, onRefresh: onRefresh, onRetry: onRetry, onSearch: onSearch, readonly: readonly, children: _jsx(RefreshErrorContext.Provider, { value: onRefreshError, children: _jsx(ErrorBoundary, { children: _jsx(Wrapper, { ...wrapperProps, children: children }) }) }) }));
|
|
23
42
|
}
|
|
24
43
|
function Header({ variant = "large", tone, actions, children, background, topSpacing, }) {
|
|
25
44
|
const { name, description, onBack } = useListView();
|
|
@@ -29,6 +48,10 @@ function HeaderSearch({ searchQuery = "" }) {
|
|
|
29
48
|
const { name, onSearch } = useListView();
|
|
30
49
|
const [query, setQuery] = useState(searchQuery);
|
|
31
50
|
const [debouncedQuery, setDebouncedQuery] = useState(searchQuery);
|
|
51
|
+
const onSearchRef = React.useRef(onSearch);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
onSearchRef.current = onSearch;
|
|
54
|
+
}, [onSearch]);
|
|
32
55
|
useEffect(() => {
|
|
33
56
|
const timeout = setTimeout(() => {
|
|
34
57
|
setDebouncedQuery(query);
|
|
@@ -36,10 +59,10 @@ function HeaderSearch({ searchQuery = "" }) {
|
|
|
36
59
|
return () => clearTimeout(timeout);
|
|
37
60
|
}, [query]);
|
|
38
61
|
useEffect(() => {
|
|
39
|
-
if (
|
|
40
|
-
|
|
62
|
+
if (debouncedQuery !== undefined) {
|
|
63
|
+
onSearchRef.current?.(debouncedQuery);
|
|
41
64
|
}
|
|
42
|
-
}, [debouncedQuery
|
|
65
|
+
}, [debouncedQuery]);
|
|
43
66
|
if (!onSearch) {
|
|
44
67
|
return null;
|
|
45
68
|
}
|
|
@@ -95,50 +118,53 @@ function FilterChip({ label, active = false, onPress }) {
|
|
|
95
118
|
function FilterChips({ children }) {
|
|
96
119
|
return (_jsx(ScrollView, { contentContainerStyle: { paddingHorizontal: 16, gap: 8 }, horizontal: true, showsHorizontalScrollIndicator: false, style: { flexGrow: 0 }, children: children }));
|
|
97
120
|
}
|
|
98
|
-
function
|
|
121
|
+
function rowKey(item) {
|
|
122
|
+
return String(item.id ?? item.index);
|
|
123
|
+
}
|
|
124
|
+
function Content({ children, withMargin = true, keyboardShouldPersistTaps = "handled", keyboardDismissMode = "none", testID, listProps, listRef, contentContainerStyle, }) {
|
|
99
125
|
const { name, items, isLoading, hasError, errorMessage, onRefresh, onRetry, onBack, showRefreshing, setShowRefreshing, } = useListView();
|
|
100
126
|
const bottomInset = useBottomInset();
|
|
101
127
|
const theme = useTheme();
|
|
128
|
+
const onRefreshError = useContext(RefreshErrorContext);
|
|
102
129
|
const handleRefresh = useCallback(async () => {
|
|
103
|
-
setShowRefreshing
|
|
104
|
-
|
|
105
|
-
await onRefresh();
|
|
106
|
-
}
|
|
107
|
-
setShowRefreshing(false);
|
|
108
|
-
}, [onRefresh, setShowRefreshing]);
|
|
130
|
+
await refreshListData(onRefresh, onRefreshError, setShowRefreshing);
|
|
131
|
+
}, [onRefresh, onRefreshError, setShowRefreshing]);
|
|
109
132
|
if (isLoading) {
|
|
110
133
|
return _jsx(Feedback.Loading, { message: `Cargando ${name}...` });
|
|
111
134
|
}
|
|
112
135
|
if (hasError) {
|
|
113
|
-
return (_jsx(ScrollView, { contentContainerStyle:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
return (_jsx(ScrollView, { contentContainerStyle: [
|
|
137
|
+
{
|
|
138
|
+
flexGrow: 1,
|
|
139
|
+
justifyContent: "center",
|
|
140
|
+
alignItems: "center",
|
|
141
|
+
paddingBottom: bottomInset,
|
|
142
|
+
},
|
|
143
|
+
contentContainerStyle,
|
|
144
|
+
], keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { backgroundColor: theme.bg.page }, testID: testID, children: _jsx(Feedback.Error, { message: errorMessage, onBack: onBack, onRetry: onRetry }) }));
|
|
119
145
|
}
|
|
120
146
|
if (items.length === 0) {
|
|
121
|
-
return (_jsx(ScrollView, { contentContainerStyle:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
return (_jsx(ScrollView, { contentContainerStyle: [
|
|
148
|
+
{
|
|
149
|
+
flexGrow: 1,
|
|
150
|
+
justifyContent: "center",
|
|
151
|
+
alignItems: "center",
|
|
152
|
+
paddingBottom: bottomInset,
|
|
153
|
+
},
|
|
154
|
+
contentContainerStyle,
|
|
155
|
+
], keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { backgroundColor: theme.bg.page }, testID: testID, children: _jsx(Feedback.Empty, {}) }));
|
|
127
156
|
}
|
|
128
|
-
return (_jsxs(View, { style: { flex: 1, backgroundColor: theme.bg.page }, children: [_jsx(Display.CountRecords, { count: items.length || 0, name: name }), _jsx(ContentList, { contentWithMargin: withMargin, children: children })] }));
|
|
157
|
+
return (_jsxs(View, { style: { flex: 1, backgroundColor: theme.bg.page }, children: [_jsx(Display.CountRecords, { count: items.length || 0, name: name }), _jsx(ContentList, { contentContainerStyle: contentContainerStyle, contentWithMargin: withMargin, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, listProps: listProps, listRef: listRef, testID: testID, children: children })] }));
|
|
129
158
|
}
|
|
130
|
-
function ContentList({ children, contentWithMargin, }) {
|
|
131
|
-
const { items, onRefresh, showRefreshing, setShowRefreshing, onPressItem, onDeleteItem, readonly, onLoadMore, loadingMore, } = useListView();
|
|
159
|
+
function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, listProps, listRef, contentContainerStyle, }) {
|
|
160
|
+
const { items, onRefresh, showRefreshing, setShowRefreshing, onPressItem, onDeleteItem, readonly, onLoadMore, loadingMore, hasMore, } = useListView();
|
|
132
161
|
const bottomInset = useBottomInset();
|
|
133
162
|
const theme = useTheme();
|
|
134
163
|
const rowItems = useMemo(() => toRowItems(items), [items]);
|
|
164
|
+
const onRefreshError = useContext(RefreshErrorContext);
|
|
135
165
|
const handleRefresh = useCallback(async () => {
|
|
136
|
-
setShowRefreshing
|
|
137
|
-
|
|
138
|
-
await onRefresh();
|
|
139
|
-
}
|
|
140
|
-
setShowRefreshing(false);
|
|
141
|
-
}, [onRefresh, setShowRefreshing]);
|
|
166
|
+
await refreshListData(onRefresh, onRefreshError, setShowRefreshing);
|
|
167
|
+
}, [onRefresh, onRefreshError, setShowRefreshing]);
|
|
142
168
|
const renderItem = useCallback(({ item }) => {
|
|
143
169
|
if (children) {
|
|
144
170
|
return _jsx(_Fragment, { children: children(item) });
|
|
@@ -153,7 +179,15 @@ function ContentList({ children, contentWithMargin, }) {
|
|
|
153
179
|
marginTop: 2,
|
|
154
180
|
paddingHorizontal: contentWithMargin ? 4 : 0,
|
|
155
181
|
backgroundColor: theme.bg.page,
|
|
156
|
-
}, children: _jsx(FlatList, {
|
|
182
|
+
}, children: _jsx(FlatList, { ...listProps, contentContainerStyle: [
|
|
183
|
+
bottomInset ? { paddingBottom: bottomInset } : undefined,
|
|
184
|
+
contentContainerStyle,
|
|
185
|
+
], data: rowItems, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, keyExtractor: rowKey, ListFooterComponent: loadingMore ? (_jsx(View, { className: "py-4", children: _jsx(Feedback.Spinner, { size: "small" }) })) : undefined, onEndReached: onLoadMore
|
|
186
|
+
? () => {
|
|
187
|
+
if (hasMore && !loadingMore && !showRefreshing)
|
|
188
|
+
onLoadMore();
|
|
189
|
+
}
|
|
190
|
+
: undefined, onEndReachedThreshold: listProps?.onEndReachedThreshold ?? 0.5, ref: listRef, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), renderItem: renderItem, scrollEnabled: true, showsVerticalScrollIndicator: false, testID: testID }) }));
|
|
157
191
|
}
|
|
158
192
|
function Footer({ children }) {
|
|
159
193
|
const { items } = useListView();
|
|
@@ -15,6 +15,7 @@ export function toRowItem(item, index, mapKeys) {
|
|
|
15
15
|
if (typeof item === "string" || typeof item === "number") {
|
|
16
16
|
return {
|
|
17
17
|
id: item,
|
|
18
|
+
index,
|
|
18
19
|
rowTitle: String(item),
|
|
19
20
|
rowSubtitle: undefined,
|
|
20
21
|
readonly: false,
|
|
@@ -22,11 +23,11 @@ export function toRowItem(item, index, mapKeys) {
|
|
|
22
23
|
}
|
|
23
24
|
const rowItem = Object.create(Object.getPrototypeOf(item));
|
|
24
25
|
Object.assign(rowItem, item, {
|
|
25
|
-
id: item[mapper.id]
|
|
26
|
-
index
|
|
27
|
-
rowTitle: item[mapper.rowTitle]
|
|
28
|
-
rowSubtitle: item[mapper.rowSubtitle]
|
|
29
|
-
readonly: item[mapper.readOnly]
|
|
26
|
+
id: item[mapper.id] ?? index,
|
|
27
|
+
index,
|
|
28
|
+
rowTitle: item[mapper.rowTitle] ?? "N/A",
|
|
29
|
+
rowSubtitle: item[mapper.rowSubtitle] ?? undefined,
|
|
30
|
+
readonly: item[mapper.readOnly] ?? false,
|
|
30
31
|
});
|
|
31
32
|
return rowItem;
|
|
32
33
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CollectionField, type CollectionQuery, type CriteriaField, type SavedCollectionQuery } from "../utils/criteria.js";
|
|
2
|
+
export interface CollectionOptionsProps {
|
|
3
|
+
visible: boolean;
|
|
4
|
+
value: CollectionQuery;
|
|
5
|
+
fields: readonly CriteriaField[];
|
|
6
|
+
sortFields?: readonly CollectionField[];
|
|
7
|
+
groupFields?: readonly CollectionField[];
|
|
8
|
+
savedQueries?: readonly SavedCollectionQuery[];
|
|
9
|
+
onApply: (query: CollectionQuery) => void;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onSave?: (label: string, query: CollectionQuery) => void | Promise<void>;
|
|
12
|
+
title?: string;
|
|
13
|
+
testID?: string;
|
|
14
|
+
}
|
|
15
|
+
export default function CollectionOptions(props: CollectionOptionsProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "nativewind/jsx-runtime";
|
|
2
|
+
import Button from "../Actions/Actions.Button.js";
|
|
3
|
+
import IconButton from "../Actions/Actions.IconButton.js";
|
|
4
|
+
import Input from "../Form/Form.Input.js";
|
|
5
|
+
import KeyboardForm from "../Form/Form.KeyboardForm.js";
|
|
6
|
+
import Pressable from "../primitives/Pressable.js";
|
|
7
|
+
import { themeToVars, useTheme } from "../theme/index.js";
|
|
8
|
+
import { Typography } from "../Typography/index.js";
|
|
9
|
+
import { describeCriteria, moveCollectionEntry, validateCriteria, } from "../utils/criteria.js";
|
|
10
|
+
import { useReducedMotion } from "../utils/useReducedMotion.js";
|
|
11
|
+
import FilterBuilder from "./FilterBuilder.js";
|
|
12
|
+
import QueryChoice from "./QueryChoice.js";
|
|
13
|
+
import { vars } from "nativewind";
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Keyboard, Modal, View } from "react-native";
|
|
16
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
17
|
+
export default function CollectionOptions(props) {
|
|
18
|
+
return props.visible ? _jsx(OptionsEditor, { ...props }) : null;
|
|
19
|
+
}
|
|
20
|
+
const sections = [
|
|
21
|
+
{ id: "filter", label: "Filtros" },
|
|
22
|
+
{ id: "sort", label: "Ordenar" },
|
|
23
|
+
{ id: "group", label: "Agrupar" },
|
|
24
|
+
{ id: "saved", label: "Guardados" },
|
|
25
|
+
];
|
|
26
|
+
function OptionsEditor({ value, fields, sortFields = [], groupFields = [], savedQueries = [], onApply, onClose, onSave, title = "Configurar colección", testID, }) {
|
|
27
|
+
const theme = useTheme();
|
|
28
|
+
const reducedMotion = useReducedMotion();
|
|
29
|
+
const insets = useSafeAreaInsets();
|
|
30
|
+
const [draft, setDraft] = useState(value);
|
|
31
|
+
const [section, setSection] = useState("filter");
|
|
32
|
+
const [editing, setEditing] = useState(false);
|
|
33
|
+
const [label, setLabel] = useState("");
|
|
34
|
+
const [error, setError] = useState();
|
|
35
|
+
const [status, setStatus] = useState();
|
|
36
|
+
const [saving, setSaving] = useState(false);
|
|
37
|
+
const problem = () => validateCriteria(draft.filter, fields) ??
|
|
38
|
+
(draft.sort.some((sort) => !sortFields.some((field) => field.id === sort.field))
|
|
39
|
+
? "El orden contiene un campo no disponible."
|
|
40
|
+
: undefined) ??
|
|
41
|
+
(draft.groupBy.some((id) => !groupFields.some((field) => field.id === id))
|
|
42
|
+
? "El agrupado contiene un campo no disponible."
|
|
43
|
+
: undefined);
|
|
44
|
+
const close = () => {
|
|
45
|
+
if (!saving) {
|
|
46
|
+
Keyboard.dismiss();
|
|
47
|
+
onClose();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const ordered = section === "sort" ? draft.sort.map((entry) => entry.field) : draft.groupBy;
|
|
51
|
+
const available = section === "sort" ? sortFields : groupFields;
|
|
52
|
+
const updateOrder = (next) => setDraft((current) => section === "sort"
|
|
53
|
+
? {
|
|
54
|
+
...current,
|
|
55
|
+
sort: next.map((field) => current.sort.find((entry) => entry.field === field) ?? {
|
|
56
|
+
field,
|
|
57
|
+
direction: "asc",
|
|
58
|
+
}),
|
|
59
|
+
}
|
|
60
|
+
: { ...current, groupBy: next });
|
|
61
|
+
return (_jsx(Modal, { animationType: reducedMotion ? "none" : "slide", onRequestClose: close, presentationStyle: "fullScreen", visible: true, children: _jsxs(View, { accessibilityViewIsModal: true, onAccessibilityEscape: close, style: [
|
|
62
|
+
vars(themeToVars(theme)),
|
|
63
|
+
{
|
|
64
|
+
flex: 1,
|
|
65
|
+
backgroundColor: theme.bg.page,
|
|
66
|
+
paddingTop: insets.top,
|
|
67
|
+
paddingBottom: insets.bottom,
|
|
68
|
+
},
|
|
69
|
+
], testID: testID, children: [_jsxs(View, { className: "flex-row items-center justify-between gap-3 px-4", children: [_jsx(Typography.Text, { accessibilityRole: "header", semibold: true, style: { flex: 1, minWidth: 0 }, variant: "bodyReadable", children: title }), _jsx(IconButton, { accessibilityLabel: "Cancelar configuraci\u00F3n", disabled: saving, icon: "close", onPress: close })] }), _jsx(View, { accessibilityRole: "tablist", className: "flex-row flex-wrap px-4 gap-2", children: sections.map((item) => (_jsx(Pressable, { accessibilityLabel: item.label, accessibilityRole: "tab", accessibilityState: {
|
|
70
|
+
selected: item.id === section,
|
|
71
|
+
disabled: editing || saving,
|
|
72
|
+
}, disabled: editing || saving, onPress: () => {
|
|
73
|
+
Keyboard.dismiss();
|
|
74
|
+
setSection(item.id);
|
|
75
|
+
setError(undefined);
|
|
76
|
+
setStatus(undefined);
|
|
77
|
+
}, style: { minHeight: 48, justifyContent: "center", paddingHorizontal: 8 }, children: _jsx(Typography.Text, { className: section === item.id ? "text-text-accent" : "text-text-secondary", semibold: section === item.id, variant: "bodyLarge", children: item.label }) }, item.id))) }), _jsxs(KeyboardForm, { contentContainerStyle: { padding: 16, gap: 16 }, children: [!editing ? (_jsx(Typography.Text, { className: "text-text-secondary", variant: "bodyLarge", children: describeCriteria(draft.filter, fields) })) : null, section === "filter" ? (_jsx(FilterBuilder, { fields: fields, onChange: (filter) => {
|
|
78
|
+
setError(undefined);
|
|
79
|
+
setDraft({ ...draft, filter });
|
|
80
|
+
}, onEditingChange: setEditing, value: draft.filter })) : section === "saved" ? (_jsxs(_Fragment, { children: [_jsx(Typography.Text, { variant: "bodyReadable", children: "Una configuraci\u00F3n incluye b\u00FAsqueda, condiciones, orden y niveles de agrupaci\u00F3n. Cargarla todav\u00EDa no modifica la colecci\u00F3n." }), !savedQueries.length ? (_jsx(Typography.Text, { children: "No hay configuraciones guardadas." })) : (savedQueries.map((saved) => (_jsx(Button, { disabled: saving, onPress: () => {
|
|
81
|
+
setDraft(saved.query);
|
|
82
|
+
setError(undefined);
|
|
83
|
+
setStatus(`Configuración ${saved.label} cargada. Pulsa Aplicar configuración.`);
|
|
84
|
+
}, title: `Cargar ${saved.label}`, variant: "secondary" }, saved.id)))), onSave ? (_jsxs(_Fragment, { children: [_jsx(Input, { editable: !saving, label: "Nombre de la configuraci\u00F3n", onChangeText: setLabel, testID: "collection-query-name", value: label }), _jsx(Button, { loading: saving, onPress: () => {
|
|
85
|
+
const invalid = problem();
|
|
86
|
+
if (invalid || !label.trim()) {
|
|
87
|
+
setError(invalid ?? "Escribe un nombre para guardar.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
setError(undefined);
|
|
91
|
+
setStatus(undefined);
|
|
92
|
+
setSaving(true);
|
|
93
|
+
Promise.resolve()
|
|
94
|
+
.then(() => onSave(label.trim(), draft))
|
|
95
|
+
.then(() => {
|
|
96
|
+
setStatus("Configuración guardada. Pulsa Aplicar configuración para usarla.");
|
|
97
|
+
}, (cause) => {
|
|
98
|
+
console.error("[CollectionOptions] Save failed", cause);
|
|
99
|
+
setError("No se pudo guardar. Conservamos la configuración para reintentar.");
|
|
100
|
+
})
|
|
101
|
+
.finally(() => setSaving(false));
|
|
102
|
+
}, title: "Guardar configuraci\u00F3n" })] })) : null] })) : (_jsxs(_Fragment, { children: [_jsx(Typography.Text, { variant: "bodyReadable", children: section === "sort"
|
|
103
|
+
? "El primer campo tiene prioridad; los siguientes resuelven empates."
|
|
104
|
+
: "Los grupos se anidan en este orden. La app aplica los niveles al conjunto completo." }), !ordered.length ? (_jsx(Typography.Text, { children: section === "sort" ? "Sin orden configurado." : "Sin agrupación." })) : null, ordered.map((id, index) => {
|
|
105
|
+
const field = available.find((item) => item.id === id);
|
|
106
|
+
const sort = draft.sort.find((entry) => entry.field === id);
|
|
107
|
+
return (_jsxs(View, { className: "gap-2 border-b border-border-light pb-3", children: [_jsxs(Typography.Text, { semibold: true, variant: "bodyReadable", children: [index + 1, ". ", field?.label ?? `${id} (no disponible)`] }), section === "sort" ? (_jsx(Button, { onPress: () => setDraft({
|
|
108
|
+
...draft,
|
|
109
|
+
sort: draft.sort.map((entry) => entry.field === id
|
|
110
|
+
? {
|
|
111
|
+
...entry,
|
|
112
|
+
direction: entry.direction === "asc" ? "desc" : "asc",
|
|
113
|
+
}
|
|
114
|
+
: entry),
|
|
115
|
+
}), title: `${field?.label ?? id}: ${sort?.direction === "asc" ? (field?.ascLabel ?? "Ascendente") : (field?.descLabel ?? "Descendente")}`, variant: "secondary" })) : null, _jsxs(View, { className: "flex-row flex-wrap gap-2", children: [_jsx(Button, { accessibilityLabel: `Subir ${field?.label ?? id}`, disabled: index === 0, onPress: () => updateOrder(moveCollectionEntry(ordered, index, -1)), title: "Subir", variant: "link" }), _jsx(Button, { accessibilityLabel: `Bajar ${field?.label ?? id}`, disabled: index === ordered.length - 1, onPress: () => updateOrder(moveCollectionEntry(ordered, index, 1)), title: "Bajar", variant: "link" }), _jsx(Button, { accessibilityLabel: `Quitar ${field?.label ?? id}`, onPress: () => updateOrder(ordered.filter((key) => key !== id)), title: "Quitar", variant: "link" })] })] }, id));
|
|
116
|
+
}), available.some((field) => !ordered.includes(field.id)) ? (_jsx(QueryChoice, { label: section === "sort"
|
|
117
|
+
? "Añadir criterio de orden"
|
|
118
|
+
: "Añadir nivel de agrupación", onChange: (id) => updateOrder([...ordered, id]), options: available
|
|
119
|
+
.filter((field) => !ordered.includes(field.id))
|
|
120
|
+
.map((field) => ({ label: field.label, value: field.id })), value: undefined })) : null] })), status ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", children: status })) : null, error ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-danger", children: error })) : null] }), _jsxs(View, { className: "px-4 py-3 border-t border-border-light gap-2", children: [editing ? (_jsx(Typography.Text, { children: "Guarda o cancela la condici\u00F3n antes de aplicar." })) : null, _jsx(Button, { disabled: editing || saving, onPress: () => {
|
|
121
|
+
const invalid = problem();
|
|
122
|
+
if (invalid) {
|
|
123
|
+
setError(invalid);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
Keyboard.dismiss();
|
|
127
|
+
onApply(draft);
|
|
128
|
+
}, title: "Aplicar configuraci\u00F3n" })] })] }) }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type SearchBarProps } from "../Navigation/Navigation.SearchBar.js";
|
|
2
|
+
export interface FilterBarOption {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
selected: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface FilterBarProps {
|
|
9
|
+
filters: readonly FilterBarOption[];
|
|
10
|
+
onFilterPress: (id: string) => void;
|
|
11
|
+
search?: SearchBarProps;
|
|
12
|
+
onOpenFilters?: () => void;
|
|
13
|
+
filtersLabel?: string;
|
|
14
|
+
onClearFilters?: () => void;
|
|
15
|
+
clearLabel?: string;
|
|
16
|
+
resultsLabel?: string;
|
|
17
|
+
layout?: "auto" | "scroll" | "wrap";
|
|
18
|
+
className?: string;
|
|
19
|
+
testID?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function FilterBar({ filters, onFilterPress, search, onOpenFilters, filtersLabel, onClearFilters, clearLabel, resultsLabel, layout, className, testID, }: FilterBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
|
|
2
|
+
import { FilterButton, FilterButtonGroup, } from "../Actions/Actions.FilterButton.js";
|
|
3
|
+
import IconButton from "../Actions/Actions.IconButton.js";
|
|
4
|
+
import SearchBar from "../Navigation/Navigation.SearchBar.js";
|
|
5
|
+
import Pressable from "../primitives/Pressable.js";
|
|
6
|
+
import { cn } from "../theme/tw.js";
|
|
7
|
+
import { Typography } from "../Typography/index.js";
|
|
8
|
+
import { useWindowDimensions, View } from "react-native";
|
|
9
|
+
export function FilterBar({ filters, onFilterPress, search, onOpenFilters, filtersLabel = "Más filtros", onClearFilters, clearLabel = "Limpiar filtros", resultsLabel, layout = "auto", className, testID, }) {
|
|
10
|
+
const { fontScale } = useWindowDimensions();
|
|
11
|
+
const selectedCount = filters.filter((filter) => filter.selected).length;
|
|
12
|
+
const filterLayout = layout === "auto" ? (fontScale > 1.3 ? "wrap" : "scroll") : layout;
|
|
13
|
+
return (_jsxs(View, { className: cn("gap-2", className), testID: testID, children: [search || onOpenFilters ? (_jsxs(View, { className: "flex-row items-center gap-2", children: [search ? (_jsx(View, { className: "flex-1 min-w-0", children: _jsx(SearchBar, { ...search }) })) : null, onOpenFilters ? (_jsx(IconButton, { accessibilityLabel: filtersLabel, icon: "sliders", onPress: onOpenFilters })) : null] })) : null, _jsx(FilterButtonGroup, { className: "p-0", layout: filterLayout, testID: testID ? `${testID}-options` : undefined, children: filters.map((filter) => (_jsx(FilterButton, { active: filter.selected, disabled: filter.disabled, onPress: () => onFilterPress(filter.id), testID: testID ? `${testID}-${filter.id}` : undefined, title: filter.label, variant: "mobile" }, filter.id))) }), resultsLabel || (selectedCount > 0 && onClearFilters) ? (_jsxs(View, { className: "flex-row flex-wrap items-center justify-between gap-x-3", children: [resultsLabel ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-text-secondary", variant: "bodyLarge", children: resultsLabel })) : null, selectedCount > 0 && onClearFilters ? (_jsx(Pressable, { accessibilityLabel: clearLabel, accessibilityRole: "button", onPress: onClearFilters, style: { minHeight: 48, minWidth: 48, justifyContent: "center" }, children: _jsx(Typography.Text, { className: "text-text-accent", variant: "bodyLarge", children: clearLabel }) })) : null] })) : null] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CriteriaField, type CriteriaGroup } from "../utils/criteria.js";
|
|
2
|
+
export interface FilterBuilderProps {
|
|
3
|
+
fields: readonly CriteriaField[];
|
|
4
|
+
value: CriteriaGroup;
|
|
5
|
+
onChange: (value: CriteriaGroup) => void;
|
|
6
|
+
onEditingChange?: (editing: boolean) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
testID?: string;
|
|
9
|
+
}
|
|
10
|
+
export default function FilterBuilder({ fields, value, onChange, onEditingChange, className, testID, }: FilterBuilderProps): import("react").JSX.Element;
|