@controleonline/ui-default 1.0.264 → 1.0.265

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/AGENTS.md +3 -2
  2. package/package.json +21 -18
  3. package/src/react/components/errors/DefaultErrors.js +64 -82
  4. package/src/react/components/filters/CompactFilterSelector.js +58 -23
  5. package/src/react/components/filters/CompactFilterSelector.styles.js +17 -5
  6. package/src/react/components/filters/DefaultColumnFilter.js +177 -45
  7. package/src/react/components/filters/DefaultExternalFilters.js +201 -143
  8. package/src/react/components/filters/DefaultSearch.js +27 -25
  9. package/src/react/components/form/DefaultForm.js +72 -53
  10. package/src/react/components/form/DefaultForm.styles.js +71 -66
  11. package/src/react/components/inputs/DefaultDateInput.js +150 -0
  12. package/src/react/components/inputs/DefaultInput.js +44 -11
  13. package/src/react/components/inputs/DefaultInput.styles.js +6 -0
  14. package/src/react/components/inputs/DefaultSelect.js +200 -43
  15. package/src/react/components/inputs/defaultInputUtils.js +40 -19
  16. package/src/react/components/table/DefaultColumnMenu.js +108 -0
  17. package/src/react/components/table/DefaultDebug.js +233 -0
  18. package/src/react/components/table/DefaultFiltersModal.js +142 -0
  19. package/src/react/components/table/DefaultModalButton.js +18 -0
  20. package/src/react/components/table/DefaultSearchModal.js +49 -0
  21. package/src/react/components/table/DefaultTable.js +350 -2025
  22. package/src/react/components/table/DefaultTable.styles.js +217 -97
  23. package/src/react/components/table/DefaultTable.utils.js +502 -0
  24. package/src/react/components/table/DefaultTableBody.js +26 -0
  25. package/src/react/components/table/DefaultTableCards.js +171 -0
  26. package/src/react/components/table/DefaultTableControls.js +208 -0
  27. package/src/react/components/table/DefaultTableEmptyState.js +22 -0
  28. package/src/react/components/table/DefaultTableFooter.js +172 -0
  29. package/src/react/components/table/DefaultTableInput.js +145 -0
  30. package/src/react/components/table/DefaultTableRows.js +340 -0
  31. package/src/react/components/table/DefaultTableSearch.js +100 -0
  32. package/src/react/components/table/DefaultTableToolbar.js +98 -0
  33. package/src/react/components/table/DefaultToolbarAction.js +56 -0
  34. package/src/react/components/table/useDefaultTablePagination.js +370 -0
  35. package/src/react/components/table/useDefaultTableSorting.js +126 -0
  36. package/src/react/components/table/useDefaultTableTheme.js +58 -0
  37. package/src/react/utils/tableVisibleColumnsPreferences.js +239 -211
  38. package/src/store/default/actions.js +51 -13
  39. package/src/store/default/getters.js +10 -8
  40. package/src/store/default/mutation_types.js +4 -2
  41. package/src/store/default/mutations.js +10 -1
  42. package/src/tests/react/components/form/DefaultForm.test.js +81 -0
  43. package/src/tests/react/components/inputs/DefaultSelect.test.js +106 -14
  44. package/src/tests/react/components/table/DefaultTable.test.js +945 -96
  45. package/src/tests/react/store/defaultActions.test.js +59 -0
  46. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +247 -168
@@ -1,6 +1,6 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import {
3
- Modal,
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Modal,
4
4
  ScrollView,
5
5
  Text,
6
6
  TextInput,
@@ -8,59 +8,85 @@ import {
8
8
  TouchableWithoutFeedback,
9
9
  View,
10
10
  useWindowDimensions,
11
- } from 'react-native';
12
- import Icon from 'react-native-vector-icons/Feather';
13
- import { useStore } from '@store';
14
- import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
15
- import { colors } from '@controleonline/../../src/styles/colors';
11
+ } from 'react-native';
12
+ import Icon from 'react-native-vector-icons/Feather';
13
+ import { getAllStores, useStore } from '@store';
14
+ import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
15
+ import { colors } from '@controleonline/../../src/styles/colors';
16
+ import {
17
+ buildOptionsFromColumn,
18
+ getColumnKey,
19
+ isDateLikeColumn,
20
+ resolveStoreNameFromList,
21
+ } from '../inputs/defaultInputUtils';
16
22
  import CompactFilterSelector from './CompactFilterSelector';
17
23
  import DateShortcutFilter from './DateShortcutFilter';
18
24
  import { resolveNextDateFilterValue } from './dateFilterSelection';
19
25
  import styles from './DefaultExternalFilters.styles';
20
26
 
21
- const DEFAULT_COMPACT_BREAKPOINT = 768;
22
- const noop = () => {};
23
-
24
- const normalizeText = value => String(value || '').trim();
25
- const DEFAULT_TRANSLATABLE_FIELDS = new Set([
26
- 'active',
27
- 'app',
28
- 'channel',
29
- 'displaytype',
30
- 'featured',
31
- 'frequency',
32
- 'invoicetype',
33
- 'installments',
34
- 'ordertype',
35
- 'peopletype',
36
- 'pricecalculation',
37
- 'productcondition',
38
- 'realstatus',
39
- 'status',
40
- ]);
41
-
42
- const getColumnKey = column => column?.key || column?.name || '';
43
-
44
- const normalizeColumnKey = value =>
45
- normalizeText(value)
46
- .replace(/[^a-zA-Z0-9]/g, '')
47
- .toLowerCase();
48
-
49
- const shouldTranslateOptionLabel = (column, storeName, rawLabel) => {
50
- if (column?.translate === false || !normalizeText(rawLabel) || !normalizeText(storeName)) {
51
- return false;
52
- }
53
-
54
- if (column?.translate === true || Array.isArray(column?.list)) {
55
- return true;
56
- }
57
-
58
- return [column?.key, column?.name, column?.label]
59
- .map(normalizeColumnKey)
60
- .filter(Boolean)
61
- .some(candidate => DEFAULT_TRANSLATABLE_FIELDS.has(candidate));
62
- };
63
-
27
+ const DEFAULT_COMPACT_BREAKPOINT = 768;
28
+ const noop = () => {};
29
+
30
+ const normalizeText = value => String(value || '').trim();
31
+ const resolveListActionName = list =>
32
+ normalizeText(list).split('/')[1] || 'getItems';
33
+
34
+ const COMPANY_SCOPED_LIST_STORES = new Set([
35
+ 'categories',
36
+ 'paymentType',
37
+ 'wallet',
38
+ ]);
39
+
40
+ const resolveListLoadParams = ({currentCompanyId, listStoreName}) => {
41
+ if (!currentCompanyId || !COMPANY_SCOPED_LIST_STORES.has(listStoreName)) {
42
+ return {};
43
+ }
44
+
45
+ return listStoreName === 'categories'
46
+ ? {company: currentCompanyId}
47
+ : {people: currentCompanyId};
48
+ };
49
+
50
+ const resolveColumnListLoadParams = ({
51
+ column,
52
+ currentCompanyId,
53
+ requestParams = {},
54
+ searchValue = '',
55
+ }) => {
56
+ const listStoreName = resolveStoreNameFromList(column?.list);
57
+ const resolvedCustomParams = typeof column?.listRequestParams === 'function'
58
+ ? column.listRequestParams({currentCompanyId, requestParams})
59
+ : column?.listRequestParams;
60
+ const customParams =
61
+ resolvedCustomParams &&
62
+ typeof resolvedCustomParams === 'object' &&
63
+ !Array.isArray(resolvedCustomParams)
64
+ ? resolvedCustomParams
65
+ : {};
66
+
67
+ return {
68
+ ...resolveListLoadParams({currentCompanyId, listStoreName}),
69
+ ...customParams,
70
+ ...(normalizeText(searchValue)
71
+ ? {[column?.listSearchParam || column?.searchParam || 'search']: normalizeText(searchValue)}
72
+ : {}),
73
+ };
74
+ };
75
+
76
+ const stableSerialize = value => {
77
+ if (value === null || value === undefined) return 'null';
78
+ if (Array.isArray(value)) return `[${value.map(item => stableSerialize(item)).join(',')}]`;
79
+ if (value instanceof Date) return JSON.stringify(value.toISOString());
80
+ if (typeof value === 'object') {
81
+ return `{${Object.keys(value)
82
+ .sort()
83
+ .map(key => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
84
+ .join(',')}}`;
85
+ }
86
+
87
+ return JSON.stringify(value);
88
+ };
89
+
64
90
  const shouldIncludeColumn = column =>
65
91
  Boolean(getColumnKey(column)) &&
66
92
  column?.show !== false &&
@@ -69,13 +95,13 @@ const shouldIncludeColumn = column =>
69
95
  column?.filter !== false &&
70
96
  column?.filters !== false;
71
97
 
72
- const normalizeFilterValue = value => {
73
- if (value && typeof value === 'object') {
74
- return normalizeFilterValue(value.value ?? value.id ?? value['@id'] ?? '');
75
- }
76
-
77
- return normalizeText(value);
78
- };
98
+ const normalizeFilterValue = value => {
99
+ if (value && typeof value === 'object') {
100
+ return normalizeFilterValue(value.value ?? value.id ?? value['@id'] ?? value.key ?? '');
101
+ }
102
+
103
+ return normalizeText(value);
104
+ };
79
105
 
80
106
  const isFilledFilterValue = value => {
81
107
  if (Array.isArray(value)) return value.length > 0;
@@ -86,52 +112,6 @@ const isFilledFilterValue = value => {
86
112
  return normalizeText(value) !== '';
87
113
  };
88
114
 
89
- const resolveOptionLabel = (column, option, storeName = '') => {
90
- if (!option) return '';
91
-
92
- const translateOptionLabel = rawLabel => {
93
- const normalizedLabel = normalizeText(rawLabel);
94
- if (!shouldTranslateOptionLabel(column, storeName, normalizedLabel)) {
95
- return normalizedLabel;
96
- }
97
-
98
- return global.t?.t(storeName, 'label', normalizedLabel) || normalizedLabel;
99
- };
100
-
101
- if (typeof column?.formatList === 'function') {
102
- const formatted = column.formatList(option, null, column);
103
- if (formatted && typeof formatted === 'object') {
104
- return translateOptionLabel(formatted.label ?? formatted.value);
105
- }
106
- if (formatted) return translateOptionLabel(formatted);
107
- }
108
-
109
- const rawLabel = normalizeText(
110
- option.label ??
111
- option[column?.searchParam] ??
112
- option[column?.name] ??
113
- option.name ??
114
- option.status ??
115
- option.wallet ??
116
- option.paymentType ??
117
- option.alias ??
118
- option.id,
119
- );
120
-
121
- return translateOptionLabel(rawLabel);
122
- };
123
-
124
- const buildColumnOptions = (column, options = [], storeName = '') => [
125
- {
126
- key: '',
127
- label: global.t?.t(storeName || 'invoice', 'label', 'select'),
128
- },
129
- ...(Array.isArray(options) ? options : []).map(option => ({
130
- key: normalizeFilterValue(option),
131
- label: resolveOptionLabel(column, option, storeName) || '-',
132
- })),
133
- ];
134
-
135
115
  const resolveDateState = filterValue => {
136
116
  if (!filterValue || typeof filterValue !== 'object') {
137
117
  return {
@@ -167,25 +147,27 @@ const resolveIcon = column => {
167
147
  const key = getColumnKey(column);
168
148
  if (key === 'status') return 'check-circle';
169
149
  if (key === 'category') return 'tag';
170
- if (column?.inputType === 'date-range' || column?.type === 'range-date') return 'calendar';
150
+ if (isDateLikeColumn(column)) return 'calendar';
171
151
  return 'sliders';
172
152
  };
173
153
 
174
154
  const DefaultExternalFilters = ({
175
- accentColor = null,
176
- compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
177
- columns = [],
178
- dateOptionKeys = ['all', 'today', 'yesterday', '7d', '30d', 'custom'],
179
- filters = {},
180
- getOptionsForColumn = null,
181
- onActiveCountChange = null,
182
- onChangeFilters = null,
183
- storeName = '',
184
- }) => {
185
- const { width } = useWindowDimensions();
155
+ accentColor = null,
156
+ compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
157
+ dateOptionKeys = ['all', 'today', 'yesterday', '7d', '30d', 'custom'],
158
+ filters = {},
159
+ getOptionsForColumn = null,
160
+ onActiveCountChange = null,
161
+ onChangeFilters = null,
162
+ requestParams = {},
163
+ storeName = '',
164
+ }) => {
165
+ const { width } = useWindowDimensions();
166
+ const externalFilterStore = useStore(storeName);
186
167
  const peopleStore = useStore('people');
187
168
  const themeStore = useStore('theme');
188
169
  const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false);
170
+ const loadedListStoresRef = useRef(new Set());
189
171
  const { currentCompany } = peopleStore.getters || {};
190
172
  const { colors: themeColors } = themeStore.getters;
191
173
  const themeTokens = useMemo(
@@ -208,11 +190,15 @@ const DefaultExternalFilters = ({
208
190
  const tableFilterBorderColor = themeColors.tableFilterBorder;
209
191
  const tableFilterTextColor = themeColors.tableFilterText;
210
192
  const onAccentColor = palette.secondary || palette.text;
211
- const isCompactView = width > 0 && width <= compactBreakpoint;
212
- const filterColumns = useMemo(
213
- () => columns.filter(shouldIncludeColumn),
214
- [columns],
215
- );
193
+ const isCompactView = width > 0 && width <= compactBreakpoint;
194
+ const resolvedColumns = useMemo(() => {
195
+ const storeColumns = externalFilterStore?.getters?.columns;
196
+ return Array.isArray(storeColumns) ? storeColumns : [];
197
+ }, [externalFilterStore?.getters?.columns]);
198
+ const filterColumns = useMemo(
199
+ () => resolvedColumns.filter(shouldIncludeColumn),
200
+ [resolvedColumns],
201
+ );
216
202
  const activeCount = useMemo(
217
203
  () => filterColumns.filter(column => isFilledFilterValue(filters[getColumnKey(column)])).length,
218
204
  [filterColumns, filters],
@@ -236,23 +222,80 @@ const DefaultExternalFilters = ({
236
222
  onChangeFilters?.({});
237
223
  }, [onChangeFilters]);
238
224
 
239
- useEffect(() => {
240
- if (!isCompactView) setIsFiltersModalOpen(false);
241
- }, [isCompactView]);
242
-
243
- if (filterColumns.length === 0) return null;
225
+ useEffect(() => {
226
+ if (!isCompactView) setIsFiltersModalOpen(false);
227
+ }, [isCompactView]);
228
+
229
+ useEffect(() => {
230
+ loadedListStoresRef.current.clear();
231
+ }, [currentCompany?.id]);
232
+
233
+ const loadListOptionsForColumn = useCallback((column, searchValue = '') => {
234
+ const explicitOptions = getOptionsForColumn?.(column);
235
+ if (!normalizeText(searchValue) && Array.isArray(explicitOptions) && explicitOptions.length > 0) {
236
+ return Promise.resolve([]);
237
+ }
238
+
239
+ const listStoreName = resolveStoreNameFromList(column?.list);
240
+ const actionName = resolveListActionName(column?.list);
241
+ const stores = getAllStores?.() || {};
242
+ const listStore = stores?.[listStoreName];
243
+ const listAction = listStore?.actions?.[actionName];
244
+
245
+ if (!listStoreName || typeof listAction !== 'function') {
246
+ return Promise.resolve([]);
247
+ }
248
+
249
+ const listLoadParams = resolveColumnListLoadParams({
250
+ column,
251
+ currentCompanyId: currentCompany?.id,
252
+ requestParams,
253
+ searchValue,
254
+ });
255
+ const hasScopedParams = Object.keys(listLoadParams).length > 0;
256
+
257
+ if (
258
+ !normalizeText(searchValue) &&
259
+ !hasScopedParams &&
260
+ Array.isArray(listStore?.getters?.items) &&
261
+ listStore.getters.items.length > 0
262
+ ) {
263
+ return Promise.resolve(listStore.getters.items);
264
+ }
265
+
266
+ const loadKey = `${getColumnKey(column)}:${listStoreName}:${actionName}:${stableSerialize(listLoadParams)}`;
267
+ if (loadedListStoresRef.current.has(loadKey)) {
268
+ return Promise.resolve(listStore?.getters?.items || []);
269
+ }
270
+
271
+ loadedListStoresRef.current.add(loadKey);
272
+ return Promise.resolve(
273
+ listAction({
274
+ ...listLoadParams,
275
+ __storeMeta: {
276
+ dedupeKey: `default-external-filter-list-options:${loadKey}`,
277
+ skipSystemError: true,
278
+ },
279
+ }),
280
+ ).catch(error => {
281
+ loadedListStoresRef.current.delete(loadKey);
282
+ throw error;
283
+ });
284
+ }, [currentCompany?.id, getOptionsForColumn, requestParams]);
285
+
286
+ if (filterColumns.length === 0) return null;
244
287
 
245
288
  const renderFilterField = (column, compact = false) => {
246
289
  const key = getColumnKey(column);
247
290
  const fieldStyle = compact ? styles.modalField : styles.field;
248
291
 
249
- if (column.inputType === 'date-range' || column.type === 'range-date') {
292
+ if (isDateLikeColumn(column)) {
250
293
  const dateState = resolveDateState(filters[key]);
251
294
 
252
295
  return (
253
296
  <View key={key} style={fieldStyle}>
254
- <DateShortcutFilter
255
- value={dateState.value}
297
+ <DateShortcutFilter
298
+ value={dateState.value}
256
299
  onChange={optionKey => {
257
300
  updateFilter(
258
301
  key,
@@ -268,9 +311,10 @@ const DefaultExternalFilters = ({
268
311
  });
269
312
  }}
270
313
  dense
271
- store={storeName}
272
- field={key}
273
- optionKeys={dateOptionKeys}
314
+ store={storeName}
315
+ field={key}
316
+ labelCaption={global.t?.t(storeName, 'label', column.label || key)}
317
+ optionKeys={dateOptionKeys}
274
318
  colors={{
275
319
  accent: resolvedAccentColor,
276
320
  appBg: 'transparent',
@@ -288,11 +332,22 @@ const DefaultExternalFilters = ({
288
332
  />
289
333
  </View>
290
334
  );
291
- }
292
-
293
- if (column.list) {
294
- const options = buildColumnOptions(column, getOptionsForColumn?.(column) || [], storeName);
295
- const selectedKey = normalizeFilterValue(filters[key]);
335
+ }
336
+
337
+ if (column.list) {
338
+ const listStoreName = resolveStoreNameFromList(column?.list);
339
+ const options = [
340
+ {
341
+ key: '',
342
+ label:
343
+ (column?.emptyOptionLabel
344
+ ? global.t?.t(storeName || 'invoice', 'label', column.emptyOptionLabel) || column.emptyOptionLabel
345
+ : null) ||
346
+ global.t?.t(storeName || 'invoice', 'label', 'select'),
347
+ },
348
+ ...buildOptionsFromColumn(column, getOptionsForColumn, storeName),
349
+ ];
350
+ const selectedKey = normalizeFilterValue(filters[key]);
296
351
  const selectedLabel =
297
352
  options.find(option => option.key === selectedKey)?.label ||
298
353
  options[0]?.label ||
@@ -305,11 +360,14 @@ const DefaultExternalFilters = ({
305
360
  label={selectedLabel}
306
361
  accentColor={resolvedAccentColor}
307
362
  active={Boolean(selectedKey)}
308
- dense
309
- store={storeName}
310
- field={key}
311
- options={options}
312
- selectedKey={selectedKey}
363
+ dense
364
+ store={storeName}
365
+ field={key}
366
+ onBeforeOpen={listStoreName ? () => loadListOptionsForColumn(column) : null}
367
+ onSearchChange={listStoreName ? value => loadListOptionsForColumn(column, value) : null}
368
+ options={options}
369
+ searchable
370
+ selectedKey={selectedKey}
313
371
  onSelect={optionKey => {
314
372
  updateFilter(key, optionKey);
315
373
  return true;
@@ -337,7 +395,7 @@ const DefaultExternalFilters = ({
337
395
  const filterTitle = global.t?.t(storeName, 'label', 'filters');
338
396
  const filterFields = compact => filterColumns.map(column => renderFilterField(column, compact));
339
397
 
340
- if (isCompactView) {
398
+ if (isCompactView) {
341
399
  return (
342
400
  <View style={styles.mobileWrap}>
343
401
  <TouchableOpacity
@@ -18,10 +18,13 @@ const DefaultSearch = ({
18
18
  storeName = '',
19
19
  style = null,
20
20
  value,
21
- }) => {
22
- const themeStore = useStore('theme');
23
- const themeColors = themeStore.getters.colors;
24
- const palette = useMemo(
21
+ }) => {
22
+ const store = useStore(storeName);
23
+ const themeStore = useStore('theme');
24
+ const themeColors = themeStore.getters.colors;
25
+ const resolvedFilters = filters || store?.getters?.filters || {};
26
+ const resolvedChangeFilters = onChangeFilters || store?.actions?.setFilters || null;
27
+ const palette = useMemo(
25
28
  () => ({
26
29
  buttonBackgroundSecondary: themeColors.buttonBackgroundSecondary,
27
30
  buttonBorder: themeColors.buttonBorder,
@@ -34,11 +37,11 @@ const DefaultSearch = ({
34
37
  inputText: themeColors.inputText,
35
38
  }),
36
39
  [themeColors],
37
- );
38
- const resolvedValue = useMemo(
39
- () => normalizeText(value ?? filters?.[searchKey]),
40
- [filters, searchKey, value],
41
- );
40
+ );
41
+ const resolvedValue = useMemo(
42
+ () => normalizeText(value ?? resolvedFilters?.[searchKey]),
43
+ [resolvedFilters, searchKey, value],
44
+ );
42
45
  const [draftValue, setDraftValue] = useState(resolvedValue);
43
46
 
44
47
  useEffect(() => {
@@ -48,12 +51,12 @@ const DefaultSearch = ({
48
51
  const commitSearch = nextValue => {
49
52
  const searchValue = normalizeText(nextValue ?? draftValue);
50
53
 
51
- if (typeof onChangeFilters === 'function') {
52
- const nextFilters = { ...(filters || {}) };
53
- if (searchValue) nextFilters[searchKey] = searchValue;
54
- else delete nextFilters[searchKey];
55
- onChangeFilters(nextFilters);
56
- }
54
+ if (typeof resolvedChangeFilters === 'function') {
55
+ const nextFilters = { ...(resolvedFilters || {}) };
56
+ if (searchValue) nextFilters[searchKey] = searchValue;
57
+ else delete nextFilters[searchKey];
58
+ resolvedChangeFilters(nextFilters);
59
+ }
57
60
 
58
61
  onSearch?.(searchValue);
59
62
  };
@@ -61,21 +64,20 @@ const DefaultSearch = ({
61
64
  const clearSearch = () => {
62
65
  setDraftValue('');
63
66
 
64
- if (typeof onChangeFilters === 'function') {
65
- const nextFilters = { ...(filters || {}) };
66
- delete nextFilters[searchKey];
67
- onChangeFilters(nextFilters);
68
- }
67
+ if (typeof resolvedChangeFilters === 'function') {
68
+ const nextFilters = { ...(resolvedFilters || {}) };
69
+ delete nextFilters[searchKey];
70
+ resolvedChangeFilters(nextFilters);
71
+ }
69
72
 
70
73
  onClear?.();
71
74
  onSearch?.('');
72
75
  };
73
76
 
74
- const resolvedPlaceholder =
75
- placeholder ||
76
- global.t?.t(storeName, 'input', searchKey) ||
77
- global.t?.t(storeName, 'placeholder', searchKey) ||
78
- 'Buscar...';
77
+ const resolvedPlaceholder =
78
+ placeholder ||
79
+ global.t?.t(storeName, 'input', searchKey) ||
80
+ global.t?.t(storeName, 'placeholder', searchKey);
79
81
 
80
82
  return (
81
83
  <View
@@ -1,16 +1,17 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
3
- import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
4
- import DefaultInput from '../inputs/DefaultInput';
5
- import {
6
- formatSaveValue,
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
3
+ import { useStore } from '@store';
4
+ import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
5
+ import DefaultInput from '../inputs/DefaultInput';
6
+ import {
7
+ formatSaveValue,
7
8
  getColumnKey,
8
9
  isEditableColumn,
9
10
  normalizeId,
10
- normalizeText,
11
- resolveEditValue,
12
- } from '../inputs/defaultInputUtils';
13
- import styles from './DefaultForm.styles';
11
+ normalizeText,
12
+ resolveEditValue,
13
+ } from '../inputs/defaultInputUtils';
14
+ import { createStyles } from './DefaultForm.styles';
14
15
 
15
16
  const hasValue = value => {
16
17
  if (value === null || value === undefined) return false;
@@ -45,29 +46,47 @@ const shouldIncludeColumn = (column, row, mode) => {
45
46
  return column?.visibleForm !== false;
46
47
  };
47
48
 
48
- const buildDraft = (columns, row) =>
49
- columns.reduce((draft, column) => {
50
- const fieldName = getColumnKey(column);
51
- const defaultValue = resolveDefaultValue(column, row);
52
- draft[fieldName] = resolveEditValue(column, row, defaultValue);
53
- return draft;
54
- }, {});
55
-
56
- const DefaultForm = ({
57
- accentColor = '#2563EB',
58
- actions = {},
59
- columns = [],
60
- getOptionsForColumn = null,
49
+ const buildDraft = (columns, row) =>
50
+ columns.reduce((draft, column) => {
51
+ const fieldName = getColumnKey(column);
52
+ const defaultValue = resolveDefaultValue(column, row);
53
+ draft[fieldName] = resolveEditValue(column, row, defaultValue);
54
+ return draft;
55
+ }, {});
56
+
57
+ const EMPTY_ROW = {};
58
+
59
+ const DefaultForm = ({
60
+ actions = {},
61
+ columns = [],
62
+ getOptionsForColumn = null,
61
63
  mode = 'edit',
62
64
  onBeforeOpen = null,
63
- onCancel = null,
64
- onSaved = null,
65
- row = {},
66
- storeName = '',
67
- }) => {
68
- const formColumns = useMemo(
69
- () => columns.filter(column => shouldIncludeColumn(column, row, mode)),
70
- [columns, mode, row],
65
+ onCancel = null,
66
+ onSaved = null,
67
+ row = EMPTY_ROW,
68
+ storeName = '',
69
+ }) => {
70
+ const themeStore = useStore('theme');
71
+ const { colors: themeColors } = themeStore.getters;
72
+ const palette = useMemo(
73
+ () => ({
74
+ buttonBackground: themeColors.buttonBackground,
75
+ buttonBackgroundSecondary: themeColors.buttonBackgroundSecondary,
76
+ buttonBorderSecondary: themeColors.buttonBorderSecondary,
77
+ buttonDisabledBackground: themeColors.buttonDisabledBackground,
78
+ buttonDisabledText: themeColors.buttonDisabledText,
79
+ buttonText: themeColors.buttonText,
80
+ buttonTextSecondary: themeColors.buttonTextSecondary,
81
+ dividerBorder: themeColors.dividerBorder,
82
+ textSecondary: themeColors.textSecondary,
83
+ }),
84
+ [themeColors],
85
+ );
86
+ const styles = useMemo(() => createStyles(palette), [palette]);
87
+ const formColumns = useMemo(
88
+ () => columns.filter(column => shouldIncludeColumn(column, row, mode)),
89
+ [columns, mode, row],
71
90
  );
72
91
  const [draft, setDraft] = useState(() => buildDraft(formColumns, row));
73
92
  const [isSaving, setIsSaving] = useState(false);
@@ -142,11 +161,11 @@ const DefaultForm = ({
142
161
 
143
162
  return (
144
163
  <View key={fieldName} style={styles.formField}>
145
- <DefaultInput
146
- accentColor={accentColor}
147
- autoFocus={false}
148
- autoSave={false}
149
- column={column}
164
+ <DefaultInput
165
+ accentColor={themeColors.buttonBackground}
166
+ autoFocus={false}
167
+ autoSave={false}
168
+ column={column}
150
169
  columns={columns}
151
170
  editing
152
171
  getOptionsForColumn={getOptionsForColumn}
@@ -172,26 +191,26 @@ const DefaultForm = ({
172
191
  )}
173
192
  </ScrollView>
174
193
 
175
- <View style={styles.actions}>
176
- <TouchableOpacity style={styles.secondaryButton} onPress={onCancel}>
177
- <Text style={styles.secondaryButtonText}>
194
+ <View style={styles.actions}>
195
+ <TouchableOpacity style={styles.secondaryButton} onPress={onCancel}>
196
+ <Text style={styles.secondaryButtonText}>
178
197
  {global.t?.t(storeName, 'button', 'cancel') || 'Cancelar'}
179
198
  </Text>
180
199
  </TouchableOpacity>
181
- <TouchableOpacity
182
- style={[
183
- styles.primaryButton,
184
- { backgroundColor: accentColor },
185
- isSaving ? styles.primaryButtonDisabled : null,
186
- ]}
187
- activeOpacity={0.86}
188
- disabled={isSaving}
189
- onPress={save}
190
- >
191
- <Text style={styles.primaryButtonText}>
192
- {isSaving ? 'Salvando' : global.t?.t(storeName, 'button', 'save') || 'Salvar'}
193
- </Text>
194
- </TouchableOpacity>
200
+ <TouchableOpacity
201
+ style={[
202
+ styles.primaryButton,
203
+ { backgroundColor: palette.buttonBackground },
204
+ isSaving ? styles.primaryButtonDisabled : null,
205
+ ]}
206
+ activeOpacity={0.86}
207
+ disabled={isSaving}
208
+ onPress={save}
209
+ >
210
+ <Text style={[styles.primaryButtonText, isSaving ? styles.primaryButtonTextDisabled : null]}>
211
+ {isSaving ? 'Salvando' : global.t?.t(storeName, 'button', 'save') || 'Salvar'}
212
+ </Text>
213
+ </TouchableOpacity>
195
214
  </View>
196
215
  </>
197
216
  );