@sincpro/mobile-ui 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +572 -229
- package/dist/Actions/Actions.Button.d.ts +7 -2
- package/dist/Actions/Actions.Button.js +23 -10
- package/dist/Actions/Actions.FAB.d.ts +3 -1
- package/dist/Actions/Actions.FAB.js +7 -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 +5 -2
- package/dist/Actions/Actions.IconButton.js +7 -3
- package/dist/Actions/index.d.ts +2 -2
- package/dist/Actions/index.js +1 -1
- package/dist/Dialog/Dialog.ActionSheet.d.ts +2 -1
- package/dist/Dialog/Dialog.ActionSheet.js +15 -5
- package/dist/Dialog/Dialog.Confirmation.js +3 -3
- 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 +14 -8
- package/dist/Display/Display.CopyableText.d.ts +9 -2
- package/dist/Display/Display.CopyableText.js +75 -7
- package/dist/Display/Display.Icon.js +2 -1
- 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 +16 -2
- package/dist/Display/Display.ListItem.js +25 -8
- package/dist/Display/Display.SectionHeader.d.ts +11 -1
- package/dist/Display/Display.SectionHeader.js +11 -2
- package/dist/Display/Display.SwipeableRow.d.ts +3 -1
- package/dist/Display/Display.SwipeableRow.js +3 -3
- package/dist/Display/index.d.ts +4 -0
- package/dist/Display/index.js +2 -0
- package/dist/Feedback/Feedback.Banner.js +1 -1
- package/dist/Feedback/Feedback.EmptyState.d.ts +2 -1
- package/dist/Feedback/Feedback.EmptyState.js +2 -2
- package/dist/Feedback/Feedback.Error.d.ts +3 -1
- package/dist/Feedback/Feedback.Error.js +4 -3
- package/dist/Form/Form.Checkbox.d.ts +4 -1
- package/dist/Form/Form.Checkbox.js +2 -2
- package/dist/Form/Form.DatePicker.d.ts +2 -1
- package/dist/Form/Form.DatePicker.js +4 -3
- package/dist/Form/Form.FilterButton.d.ts +1 -1
- package/dist/Form/Form.Input.d.ts +5 -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 +3 -1
- package/dist/Form/Form.Select.js +23 -10
- package/dist/Form/index.d.ts +6 -0
- package/dist/Form/index.js +4 -0
- package/dist/Navigation/Navigation.AppBar.d.ts +6 -2
- package/dist/Navigation/Navigation.AppBar.js +7 -7
- package/dist/Navigation/Navigation.BottomNav.d.ts +3 -1
- package/dist/Navigation/Navigation.BottomNav.js +9 -5
- package/dist/Navigation/Navigation.RowItem.d.ts +1 -1
- package/dist/Navigation/Navigation.RowItem.js +2 -2
- package/dist/Navigation/Navigation.SearchBar.d.ts +6 -1
- package/dist/Navigation/Navigation.SearchBar.js +29 -8
- package/dist/Navigation/Navigation.SearchSuggestions.d.ts +14 -0
- package/dist/Navigation/Navigation.SearchSuggestions.js +7 -0
- package/dist/Navigation/Navigation.SegmentedControl.d.ts +3 -1
- package/dist/Navigation/Navigation.SegmentedControl.js +2 -1
- package/dist/Navigation/index.d.ts +6 -2
- package/dist/Navigation/index.js +2 -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/layouts/AuthContainer.d.ts +4 -1
- package/dist/layouts/AuthContainer.js +3 -3
- package/dist/primitives/BaseButton.d.ts +7 -2
- package/dist/primitives/BaseButton.js +19 -5
- 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/reorder.d.ts +4 -0
- package/dist/utils/reorder.js +32 -0
- package/dist/views/AuthFormView.d.ts +4 -1
- package/dist/views/AuthFormView.js +2 -2
- package/dist/views/FormViewV2.d.ts +7 -2
- package/dist/views/FormViewV2.js +35 -16
- package/dist/views/ListViewV2.d.ts +9 -6
- package/dist/views/ListViewV2.js +34 -21
- package/dist/widgets/CollectionOptions.d.ts +15 -0
- package/dist/widgets/CollectionOptions.js +127 -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/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/index.d.ts +4 -0
- package/dist/widgets/index.js +3 -0
- package/package.json +15 -1
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 { 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, 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, 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();
|
|
@@ -95,17 +114,14 @@ function FilterChip({ label, active = false, onPress }) {
|
|
|
95
114
|
function FilterChips({ children }) {
|
|
96
115
|
return (_jsx(ScrollView, { contentContainerStyle: { paddingHorizontal: 16, gap: 8 }, horizontal: true, showsHorizontalScrollIndicator: false, style: { flexGrow: 0 }, children: children }));
|
|
97
116
|
}
|
|
98
|
-
function Content({ children, withMargin = true, }) {
|
|
117
|
+
function Content({ children, withMargin = true, keyboardShouldPersistTaps = "handled", keyboardDismissMode = "none", testID, }) {
|
|
99
118
|
const { name, items, isLoading, hasError, errorMessage, onRefresh, onRetry, onBack, showRefreshing, setShowRefreshing, } = useListView();
|
|
100
119
|
const bottomInset = useBottomInset();
|
|
101
120
|
const theme = useTheme();
|
|
121
|
+
const onRefreshError = useContext(RefreshErrorContext);
|
|
102
122
|
const handleRefresh = useCallback(async () => {
|
|
103
|
-
setShowRefreshing
|
|
104
|
-
|
|
105
|
-
await onRefresh();
|
|
106
|
-
}
|
|
107
|
-
setShowRefreshing(false);
|
|
108
|
-
}, [onRefresh, setShowRefreshing]);
|
|
123
|
+
await refreshListData(onRefresh, onRefreshError, setShowRefreshing);
|
|
124
|
+
}, [onRefresh, onRefreshError, setShowRefreshing]);
|
|
109
125
|
if (isLoading) {
|
|
110
126
|
return _jsx(Feedback.Loading, { message: `Cargando ${name}...` });
|
|
111
127
|
}
|
|
@@ -115,7 +131,7 @@ function Content({ children, withMargin = true, }) {
|
|
|
115
131
|
justifyContent: "center",
|
|
116
132
|
alignItems: "center",
|
|
117
133
|
paddingBottom: bottomInset,
|
|
118
|
-
}, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { backgroundColor: theme.bg.page }, children: _jsx(Feedback.Error, { message: errorMessage, onBack: onBack, onRetry: onRetry }) }));
|
|
134
|
+
}, 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
135
|
}
|
|
120
136
|
if (items.length === 0) {
|
|
121
137
|
return (_jsx(ScrollView, { contentContainerStyle: {
|
|
@@ -123,22 +139,19 @@ function Content({ children, withMargin = true, }) {
|
|
|
123
139
|
justifyContent: "center",
|
|
124
140
|
alignItems: "center",
|
|
125
141
|
paddingBottom: bottomInset,
|
|
126
|
-
}, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), style: { backgroundColor: theme.bg.page }, children: _jsx(Feedback.Empty, {}) }));
|
|
142
|
+
}, 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
143
|
}
|
|
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 })] }));
|
|
144
|
+
return (_jsxs(View, { style: { flex: 1, backgroundColor: theme.bg.page }, children: [_jsx(Display.CountRecords, { count: items.length || 0, name: name }), _jsx(ContentList, { contentWithMargin: withMargin, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, testID: testID, children: children })] }));
|
|
129
145
|
}
|
|
130
|
-
function ContentList({ children, contentWithMargin, }) {
|
|
146
|
+
function ContentList({ children, contentWithMargin, keyboardShouldPersistTaps, keyboardDismissMode, testID, }) {
|
|
131
147
|
const { items, onRefresh, showRefreshing, setShowRefreshing, onPressItem, onDeleteItem, readonly, onLoadMore, loadingMore, } = useListView();
|
|
132
148
|
const bottomInset = useBottomInset();
|
|
133
149
|
const theme = useTheme();
|
|
134
150
|
const rowItems = useMemo(() => toRowItems(items), [items]);
|
|
151
|
+
const onRefreshError = useContext(RefreshErrorContext);
|
|
135
152
|
const handleRefresh = useCallback(async () => {
|
|
136
|
-
setShowRefreshing
|
|
137
|
-
|
|
138
|
-
await onRefresh();
|
|
139
|
-
}
|
|
140
|
-
setShowRefreshing(false);
|
|
141
|
-
}, [onRefresh, setShowRefreshing]);
|
|
153
|
+
await refreshListData(onRefresh, onRefreshError, setShowRefreshing);
|
|
154
|
+
}, [onRefresh, onRefreshError, setShowRefreshing]);
|
|
142
155
|
const renderItem = useCallback(({ item }) => {
|
|
143
156
|
if (children) {
|
|
144
157
|
return _jsx(_Fragment, { children: children(item) });
|
|
@@ -153,7 +166,7 @@ function ContentList({ children, contentWithMargin, }) {
|
|
|
153
166
|
marginTop: 2,
|
|
154
167
|
paddingHorizontal: contentWithMargin ? 4 : 0,
|
|
155
168
|
backgroundColor: theme.bg.page,
|
|
156
|
-
}, children: _jsx(FlatList, { contentContainerStyle: bottomInset ? { paddingBottom: bottomInset } : undefined, data: rowItems, keyExtractor: (item) => String(item.index), ListFooterComponent: loadingMore ? (_jsx(View, { className: "py-4", children: _jsx(Feedback.Spinner, { size: "small" }) })) : undefined, onEndReached: onLoadMore ? () => onLoadMore() : undefined, onEndReachedThreshold: 0.5, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), renderItem: renderItem, scrollEnabled: true, showsVerticalScrollIndicator: false }) }));
|
|
169
|
+
}, children: _jsx(FlatList, { contentContainerStyle: bottomInset ? { paddingBottom: bottomInset } : undefined, data: rowItems, keyboardDismissMode: keyboardDismissMode, keyboardShouldPersistTaps: keyboardShouldPersistTaps, keyExtractor: (item) => String(item.index), ListFooterComponent: loadingMore ? (_jsx(View, { className: "py-4", children: _jsx(Feedback.Spinner, { size: "small" }) })) : undefined, onEndReached: onLoadMore ? () => onLoadMore() : undefined, onEndReachedThreshold: 0.5, refreshControl: _jsx(RefreshControl, { colors: [theme.primary], onRefresh: handleRefresh, refreshing: showRefreshing }), renderItem: renderItem, scrollEnabled: true, showsVerticalScrollIndicator: false, testID: testID }) }));
|
|
157
170
|
}
|
|
158
171
|
function Footer({ children }) {
|
|
159
172
|
const { items } = useListView();
|
|
@@ -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,127 @@
|
|
|
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 FilterBuilder from "./FilterBuilder.js";
|
|
11
|
+
import QueryChoice from "./QueryChoice.js";
|
|
12
|
+
import { vars } from "nativewind";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Keyboard, Modal, View } from "react-native";
|
|
15
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
16
|
+
export default function CollectionOptions(props) {
|
|
17
|
+
return props.visible ? _jsx(OptionsEditor, { ...props }) : null;
|
|
18
|
+
}
|
|
19
|
+
const sections = [
|
|
20
|
+
{ id: "filter", label: "Filtros" },
|
|
21
|
+
{ id: "sort", label: "Ordenar" },
|
|
22
|
+
{ id: "group", label: "Agrupar" },
|
|
23
|
+
{ id: "saved", label: "Guardados" },
|
|
24
|
+
];
|
|
25
|
+
function OptionsEditor({ value, fields, sortFields = [], groupFields = [], savedQueries = [], onApply, onClose, onSave, title = "Configurar colección", testID, }) {
|
|
26
|
+
const theme = useTheme();
|
|
27
|
+
const insets = useSafeAreaInsets();
|
|
28
|
+
const [draft, setDraft] = useState(value);
|
|
29
|
+
const [section, setSection] = useState("filter");
|
|
30
|
+
const [editing, setEditing] = useState(false);
|
|
31
|
+
const [label, setLabel] = useState("");
|
|
32
|
+
const [error, setError] = useState();
|
|
33
|
+
const [status, setStatus] = useState();
|
|
34
|
+
const [saving, setSaving] = useState(false);
|
|
35
|
+
const problem = () => validateCriteria(draft.filter, fields) ??
|
|
36
|
+
(draft.sort.some((sort) => !sortFields.some((field) => field.id === sort.field))
|
|
37
|
+
? "El orden contiene un campo no disponible."
|
|
38
|
+
: undefined) ??
|
|
39
|
+
(draft.groupBy.some((id) => !groupFields.some((field) => field.id === id))
|
|
40
|
+
? "El agrupado contiene un campo no disponible."
|
|
41
|
+
: undefined);
|
|
42
|
+
const close = () => {
|
|
43
|
+
if (!saving) {
|
|
44
|
+
Keyboard.dismiss();
|
|
45
|
+
onClose();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const ordered = section === "sort" ? draft.sort.map((entry) => entry.field) : draft.groupBy;
|
|
49
|
+
const available = section === "sort" ? sortFields : groupFields;
|
|
50
|
+
const updateOrder = (next) => setDraft((current) => section === "sort"
|
|
51
|
+
? {
|
|
52
|
+
...current,
|
|
53
|
+
sort: next.map((field) => current.sort.find((entry) => entry.field === field) ?? {
|
|
54
|
+
field,
|
|
55
|
+
direction: "asc",
|
|
56
|
+
}),
|
|
57
|
+
}
|
|
58
|
+
: { ...current, groupBy: next });
|
|
59
|
+
return (_jsx(Modal, { animationType: "slide", onRequestClose: close, presentationStyle: "fullScreen", visible: true, children: _jsxs(View, { accessibilityViewIsModal: true, onAccessibilityEscape: close, style: [
|
|
60
|
+
vars(themeToVars(theme)),
|
|
61
|
+
{
|
|
62
|
+
flex: 1,
|
|
63
|
+
backgroundColor: theme.bg.page,
|
|
64
|
+
paddingTop: insets.top,
|
|
65
|
+
paddingBottom: insets.bottom,
|
|
66
|
+
},
|
|
67
|
+
], 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", accessibleSizing: true, 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: {
|
|
68
|
+
selected: item.id === section,
|
|
69
|
+
disabled: editing || saving,
|
|
70
|
+
}, disabled: editing || saving, onPress: () => {
|
|
71
|
+
Keyboard.dismiss();
|
|
72
|
+
setSection(item.id);
|
|
73
|
+
setError(undefined);
|
|
74
|
+
setStatus(undefined);
|
|
75
|
+
}, 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) => {
|
|
76
|
+
setError(undefined);
|
|
77
|
+
setDraft({ ...draft, filter });
|
|
78
|
+
}, 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, { accessibleSizing: true, disabled: saving, onPress: () => {
|
|
79
|
+
setDraft(saved.query);
|
|
80
|
+
setError(undefined);
|
|
81
|
+
setStatus(`Configuración ${saved.label} cargada. Pulsa Aplicar configuración.`);
|
|
82
|
+
}, title: `Cargar ${saved.label}`, variant: "secondary" }, saved.id)))), onSave ? (_jsxs(_Fragment, { children: [_jsx(Input, { accessibleSizing: true, editable: !saving, label: "Nombre de la configuraci\u00F3n", onChangeText: setLabel, testID: "collection-query-name", value: label }), _jsx(Button, { accessibleSizing: true, loading: saving, onPress: () => {
|
|
83
|
+
const invalid = problem();
|
|
84
|
+
if (invalid || !label.trim()) {
|
|
85
|
+
setError(invalid ?? "Escribe un nombre para guardar.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setError(undefined);
|
|
89
|
+
setStatus(undefined);
|
|
90
|
+
setSaving(true);
|
|
91
|
+
Promise.resolve()
|
|
92
|
+
.then(() => onSave(label.trim(), draft))
|
|
93
|
+
.then(() => {
|
|
94
|
+
setStatus("Configuración guardada. Pulsa Aplicar configuración para usarla.");
|
|
95
|
+
}, (cause) => {
|
|
96
|
+
console.error("[CollectionOptions] Save failed", cause);
|
|
97
|
+
setError("No se pudo guardar. Conservamos la configuración para reintentar.");
|
|
98
|
+
})
|
|
99
|
+
.finally(() => setSaving(false));
|
|
100
|
+
}, title: "Guardar configuraci\u00F3n" })] })) : null] })) : (_jsxs(_Fragment, { children: [_jsx(Typography.Text, { variant: "bodyReadable", children: section === "sort"
|
|
101
|
+
? "El primer campo tiene prioridad; los siguientes resuelven empates."
|
|
102
|
+
: "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) => {
|
|
103
|
+
const field = available.find((item) => item.id === id);
|
|
104
|
+
const sort = draft.sort.find((entry) => entry.field === id);
|
|
105
|
+
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, { accessibleSizing: true, onPress: () => setDraft({
|
|
106
|
+
...draft,
|
|
107
|
+
sort: draft.sort.map((entry) => entry.field === id
|
|
108
|
+
? {
|
|
109
|
+
...entry,
|
|
110
|
+
direction: entry.direction === "asc" ? "desc" : "asc",
|
|
111
|
+
}
|
|
112
|
+
: entry),
|
|
113
|
+
}), 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}`, accessibleSizing: true, disabled: index === 0, onPress: () => updateOrder(moveCollectionEntry(ordered, index, -1)), title: "Subir", variant: "link" }), _jsx(Button, { accessibilityLabel: `Bajar ${field?.label ?? id}`, accessibleSizing: true, disabled: index === ordered.length - 1, onPress: () => updateOrder(moveCollectionEntry(ordered, index, 1)), title: "Bajar", variant: "link" }), _jsx(Button, { accessibilityLabel: `Quitar ${field?.label ?? id}`, accessibleSizing: true, onPress: () => updateOrder(ordered.filter((key) => key !== id)), title: "Quitar", variant: "link" })] })] }, id));
|
|
114
|
+
}), available.some((field) => !ordered.includes(field.id)) ? (_jsx(QueryChoice, { label: section === "sort"
|
|
115
|
+
? "Añadir criterio de orden"
|
|
116
|
+
: "Añadir nivel de agrupación", onChange: (id) => updateOrder([...ordered, id]), options: available
|
|
117
|
+
.filter((field) => !ordered.includes(field.id))
|
|
118
|
+
.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, { accessibleSizing: true, disabled: editing || saving, onPress: () => {
|
|
119
|
+
const invalid = problem();
|
|
120
|
+
if (invalid) {
|
|
121
|
+
setError(invalid);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
Keyboard.dismiss();
|
|
125
|
+
onApply(draft);
|
|
126
|
+
}, title: "Aplicar configuraci\u00F3n" })] })] }) }));
|
|
127
|
+
}
|
|
@@ -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?: Omit<SearchBarProps, "accessibleSizing">;
|
|
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, accessibleSizing: true }) })) : null, onOpenFilters ? (_jsx(IconButton, { accessibilityLabel: filtersLabel, accessibleSizing: true, 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;
|
|
@@ -0,0 +1,115 @@
|
|
|
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 DatePicker from "../Form/Form.DatePicker.js";
|
|
4
|
+
import Input from "../Form/Form.Input.js";
|
|
5
|
+
import { Typography } from "../Typography/index.js";
|
|
6
|
+
import { describeCriteria, parseCriteriaDate, replaceCriteria, validateCondition, } from "../utils/criteria.js";
|
|
7
|
+
import QueryChoice from "./QueryChoice.js";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { View } from "react-native";
|
|
10
|
+
export default function FilterBuilder({ fields, value, onChange, onEditingChange, className, testID, }) {
|
|
11
|
+
const [path, setPath] = useState([]);
|
|
12
|
+
const [editing, setEditing] = useState(null);
|
|
13
|
+
const [error, setError] = useState();
|
|
14
|
+
const isEditing = editing !== null;
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
onEditingChange?.(isEditing);
|
|
17
|
+
return () => onEditingChange?.(false);
|
|
18
|
+
}, [isEditing, onEditingChange]);
|
|
19
|
+
let current = value;
|
|
20
|
+
for (const index of path) {
|
|
21
|
+
current = current?.kind === "group" ? current.children[index] : undefined;
|
|
22
|
+
}
|
|
23
|
+
if (current?.kind !== "group")
|
|
24
|
+
return (_jsxs(View, { className: className, testID: testID, children: [_jsx(Typography.Text, { accessibilityLiveRegion: "polite", children: "Este grupo ya no est\u00E1 disponible. La configuraci\u00F3n cambi\u00F3." }), _jsx(Button, { accessibleSizing: true, onPress: () => {
|
|
25
|
+
setPath([]);
|
|
26
|
+
setEditing(null);
|
|
27
|
+
}, title: "Volver a condiciones", variant: "secondary" })] }));
|
|
28
|
+
const group = current;
|
|
29
|
+
const changeGroup = (next) => onChange(replaceCriteria(value, path, next));
|
|
30
|
+
const condition = editing?.condition;
|
|
31
|
+
const field = fields.find((item) => item.id === condition?.field);
|
|
32
|
+
const operator = field?.operators.find((item) => item.id === condition?.operator);
|
|
33
|
+
const changeCondition = (next) => {
|
|
34
|
+
setError(undefined);
|
|
35
|
+
setEditing((current) => (current ? { ...current, condition: next } : null));
|
|
36
|
+
};
|
|
37
|
+
const addCondition = () => {
|
|
38
|
+
const first = fields.find((item) => item.operators.length);
|
|
39
|
+
if (!first)
|
|
40
|
+
throw new Error("No configurable filter fields");
|
|
41
|
+
setError(undefined);
|
|
42
|
+
setEditing({
|
|
43
|
+
index: null,
|
|
44
|
+
condition: {
|
|
45
|
+
kind: "condition",
|
|
46
|
+
field: first.id,
|
|
47
|
+
operator: first.operators[0].id,
|
|
48
|
+
values: [],
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
return (_jsxs(View, { className: className, style: { gap: 16 }, testID: testID, children: [_jsx(Typography.Text, { accessibilityRole: "header", semibold: true, variant: "bodyReadable", children: path.length ? `Grupo ${path.map((index) => index + 1).join(" › ")}` : "Condiciones" }), editing && condition ? (_jsxs(_Fragment, { children: [_jsx(QueryChoice, { label: "Campo", onChange: (id) => {
|
|
53
|
+
const next = fields.find((item) => item.id === id);
|
|
54
|
+
if (!next?.operators.length)
|
|
55
|
+
throw new Error("Field has no operators");
|
|
56
|
+
changeCondition({
|
|
57
|
+
...condition,
|
|
58
|
+
field: id,
|
|
59
|
+
operator: next.operators[0].id,
|
|
60
|
+
values: [],
|
|
61
|
+
});
|
|
62
|
+
}, options: fields
|
|
63
|
+
.filter((item) => item.operators.length)
|
|
64
|
+
.map((item) => ({ label: item.label, value: item.id })), value: condition.field }), _jsx(QueryChoice, { label: "Comparaci\u00F3n", onChange: (id) => changeCondition({ ...condition, operator: id, values: [] }), options: (field?.operators ?? []).map((item) => ({
|
|
65
|
+
label: item.label,
|
|
66
|
+
value: item.id,
|
|
67
|
+
})), value: condition.operator }), Array.from({ length: operator?.arity === "range" ? 2 : operator?.arity === "one" ? 1 : 0 }, (_, index) => {
|
|
68
|
+
const label = operator?.arity === "range" ? (index === 0 ? "Desde" : "Hasta") : "Valor";
|
|
69
|
+
const update = (next) => {
|
|
70
|
+
const values = [...condition.values];
|
|
71
|
+
values[index] = next;
|
|
72
|
+
changeCondition({ ...condition, values });
|
|
73
|
+
};
|
|
74
|
+
return field?.type === "choice" ? (_jsx(QueryChoice, { label: label, onChange: update, options: field.options ?? [], value: condition.values[index] }, index)) : field?.type === "date" ? (_jsx(DatePicker, { accessibleSizing: true, label: label, onChange: (date) => update(`${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`), value: parseCriteriaDate(condition.values[index]) }, index)) : (_jsx(Input, { accessibleSizing: true, keyboardType: field?.type === "number" ? "numbers-and-punctuation" : "default", label: label, onChangeText: update, testID: `criteria-value-${index}`, value: condition.values[index] === undefined ||
|
|
75
|
+
Number.isNaN(condition.values[index])
|
|
76
|
+
? ""
|
|
77
|
+
: String(condition.values[index]) }, index));
|
|
78
|
+
}), error ? (_jsx(Typography.Text, { accessibilityLiveRegion: "polite", className: "text-danger", children: error })) : null, _jsx(Button, { accessibleSizing: true, onPress: () => {
|
|
79
|
+
const next = {
|
|
80
|
+
...condition,
|
|
81
|
+
values: field?.type === "number"
|
|
82
|
+
? Array.from(condition.values, (value) => typeof value === "string" && !value.trim() ? NaN : Number(value))
|
|
83
|
+
: condition.values,
|
|
84
|
+
};
|
|
85
|
+
const problem = validateCondition(next, fields);
|
|
86
|
+
if (problem) {
|
|
87
|
+
setError(problem);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const children = [...group.children];
|
|
91
|
+
if (editing.index === null)
|
|
92
|
+
children.push(next);
|
|
93
|
+
else
|
|
94
|
+
children[editing.index] = next;
|
|
95
|
+
changeGroup({ ...group, children });
|
|
96
|
+
setEditing(null);
|
|
97
|
+
}, title: "Guardar condici\u00F3n" }), _jsx(Button, { accessibleSizing: true, onPress: () => setEditing(null), title: "Cancelar condici\u00F3n", variant: "secondary" })] })) : (_jsxs(_Fragment, { children: [path.length ? (_jsx(Button, { accessibleSizing: true, onPress: () => setPath(path.slice(0, -1)), title: "Volver al grupo anterior", variant: "link" })) : null, _jsx(QueryChoice, { label: "Coinciden", onChange: (join) => changeGroup({ ...group, join }), options: [
|
|
98
|
+
{ label: "Todas las condiciones", value: "all" },
|
|
99
|
+
{ label: "Cualquiera de las condiciones", value: "any" },
|
|
100
|
+
{ label: "Ninguna de las condiciones", value: "none" },
|
|
101
|
+
], value: group.join }), !group.children.length ? (_jsx(Typography.Text, { className: "text-text-secondary", children: "Sin condiciones en este grupo." })) : null, group.children.map((child, index) => (_jsxs(View, { className: "border-b border-border-light pb-3 gap-2", children: [_jsx(Typography.Text, { variant: "bodyReadable", children: describeCriteria(child, fields) }), _jsxs(View, { className: "flex-row flex-wrap gap-2", children: [_jsx(Button, { accessibleSizing: true, onPress: () => child.kind === "group"
|
|
102
|
+
? setPath([...path, index])
|
|
103
|
+
: (setError(undefined), setEditing({ index, condition: child })), title: child.kind === "group"
|
|
104
|
+
? `Abrir grupo ${index + 1}`
|
|
105
|
+
: `Editar condición ${index + 1}`, variant: "secondary" }), _jsx(Button, { accessibleSizing: true, onPress: () => changeGroup({
|
|
106
|
+
...group,
|
|
107
|
+
children: group.children.filter((_, position) => position !== index),
|
|
108
|
+
}), title: `Eliminar ${child.kind === "group" ? "grupo" : "condición"} ${index + 1}`, variant: "link" })] })] }, index))), _jsx(Button, { accessibleSizing: true, disabled: !fields.some((item) => item.operators.length), onPress: addCondition, title: "A\u00F1adir condici\u00F3n", variant: "secondary" }), _jsx(Button, { accessibleSizing: true, onPress: () => {
|
|
109
|
+
changeGroup({
|
|
110
|
+
...group,
|
|
111
|
+
children: [...group.children, { kind: "group", join: "any", children: [] }],
|
|
112
|
+
});
|
|
113
|
+
setPath([...path, group.children.length]);
|
|
114
|
+
}, title: "A\u00F1adir grupo", variant: "secondary" })] }))] }));
|
|
115
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface QueryChoiceProps<T extends string | number | boolean> {
|
|
2
|
+
label: string;
|
|
3
|
+
value: T | undefined;
|
|
4
|
+
options: readonly {
|
|
5
|
+
label: string;
|
|
6
|
+
value: T;
|
|
7
|
+
}[];
|
|
8
|
+
onChange: (value: T) => void;
|
|
9
|
+
}
|
|
10
|
+
export default function QueryChoice<T extends string | number | boolean>({ label, value, options, onChange, }: QueryChoiceProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
|
|
2
|
+
import Pressable from "../primitives/Pressable.js";
|
|
3
|
+
import { Typography } from "../Typography/index.js";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { View } from "react-native";
|
|
6
|
+
export default function QueryChoice({ label, value, options, onChange, }) {
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
const selected = options.find((option) => option.value === value);
|
|
9
|
+
return (_jsxs(View, { className: "gap-1", children: [_jsxs(Pressable, { accessibilityLabel: `${label}: ${selected?.label ?? "Seleccionar"}`, accessibilityState: { expanded: open }, className: "rounded-lg border border-input px-3 py-2", onPress: () => setOpen(!open), style: { minHeight: 48, justifyContent: "center" }, children: [_jsx(Typography.Text, { className: "text-text-secondary", variant: "bodySmall", children: label }), _jsxs(Typography.Text, { variant: "bodyReadable", children: [selected?.label ?? "Seleccionar", " ", open ? "−" : "+"] })] }), open ? (_jsx(View, { accessibilityRole: "radiogroup", className: "border-l border-border-default pl-3", children: options.map((option) => (_jsx(Pressable, { accessibilityLabel: option.label, accessibilityRole: "radio", accessibilityState: { checked: option.value === value }, onPress: () => {
|
|
10
|
+
onChange(option.value);
|
|
11
|
+
setOpen(false);
|
|
12
|
+
}, style: { minHeight: 48, justifyContent: "center" }, children: _jsxs(Typography.Text, { className: option.value === value ? "text-text-accent" : "text-text-primary", variant: "bodyReadable", children: [option.value === value ? "✓ " : "", option.label] }) }, String(option.value)))) })) : null] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { type SortableGridProps } from "react-native-sortables";
|
|
4
|
+
export interface ReorderEvent {
|
|
5
|
+
key: string;
|
|
6
|
+
fromIndex: number;
|
|
7
|
+
toIndex: number;
|
|
8
|
+
source: "drag" | "button";
|
|
9
|
+
}
|
|
10
|
+
export interface ReorderItemContext<T> {
|
|
11
|
+
item: T;
|
|
12
|
+
index: number;
|
|
13
|
+
canMoveUp: boolean;
|
|
14
|
+
canMoveDown: boolean;
|
|
15
|
+
moveUp: () => void;
|
|
16
|
+
moveDown: () => void;
|
|
17
|
+
}
|
|
18
|
+
export interface ReorderControlsContext<T> extends ReorderItemContext<T> {
|
|
19
|
+
dragHandle: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export interface ReorderableListProps<T> {
|
|
22
|
+
items: readonly T[];
|
|
23
|
+
keyExtractor: (item: T) => string;
|
|
24
|
+
getItemLabel: (item: T) => string;
|
|
25
|
+
onChange: (items: T[], event: ReorderEvent) => void;
|
|
26
|
+
renderItem: (context: ReorderItemContext<T>) => ReactNode;
|
|
27
|
+
renderControls?: (context: ReorderControlsContext<T>) => ReactNode;
|
|
28
|
+
renderHandleContent?: (context: ReorderItemContext<T>) => ReactNode;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
className?: string;
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
itemStyle?: StyleProp<ViewStyle>;
|
|
33
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
34
|
+
controlsStyle?: StyleProp<ViewStyle>;
|
|
35
|
+
handleStyle?: StyleProp<ViewStyle>;
|
|
36
|
+
actionStyle?: StyleProp<ViewStyle>;
|
|
37
|
+
testID?: string;
|
|
38
|
+
labels?: {
|
|
39
|
+
drag?: (label: string) => string;
|
|
40
|
+
up?: (label: string) => string;
|
|
41
|
+
down?: (label: string) => string;
|
|
42
|
+
moved?: (label: string, position: number, count: number) => string;
|
|
43
|
+
changedDuringDrag?: string;
|
|
44
|
+
disabledDuringDrag?: string;
|
|
45
|
+
};
|
|
46
|
+
onDragStart?: (key: string) => void;
|
|
47
|
+
onDragEnd?: () => void;
|
|
48
|
+
onReorderRejected?: (reason: "data-changed" | "disabled") => void;
|
|
49
|
+
sortableProps?: Omit<SortableGridProps<T>, "data" | "renderItem" | "keyExtractor" | "rows" | "rowHeight" | "columns" | "onDragStart" | "onDragEnd" | "onOrderChange" | "sortEnabled" | "customHandle" | "strategy">;
|
|
50
|
+
}
|
|
51
|
+
export default function ReorderableList<T>({ items, keyExtractor, getItemLabel, onChange, renderItem, renderControls, renderHandleContent, disabled, className, style, itemStyle, contentStyle, controlsStyle, handleStyle, actionStyle, testID, labels, onDragStart, onDragEnd, onReorderRejected, sortableProps, }: ReorderableListProps<T>): import("react").JSX.Element;
|