@controleonline/ui-default 1.0.270 → 1.0.273

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 (100) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/.github/workflows/publish-npm.yml +91 -0
  4. package/AGENTS.md +46 -50
  5. package/docs/technical/Contrato-de-colunas.md +67 -0
  6. package/docs/technical/Default-Components.md +76 -0
  7. package/docs/technical/DefaultForm.md +80 -0
  8. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  9. package/docs/technical/DefaultTable.md +272 -0
  10. package/docs/wiki.md +1 -0
  11. package/package.json +4 -3
  12. package/scripts/validate-wiki-pointer.cjs +13 -0
  13. package/src/react/components/address/DefaultAddress.js +446 -0
  14. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  15. package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
  16. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  17. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  18. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  19. package/src/react/components/address/defaultAddressStyles.js +148 -0
  20. package/src/react/components/address/defaultAddressUtils.js +129 -0
  21. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  22. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  23. package/src/react/components/files/DefaultFile.js +231 -46
  24. package/src/react/components/files/DefaultFileColumn.js +123 -0
  25. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  26. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  27. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  28. package/src/react/components/filters/DefaultColumnFilter.js +60 -48
  29. package/src/react/components/filters/DefaultExternalFilters.js +324 -265
  30. package/src/react/components/filters/filterValue.js +65 -0
  31. package/src/react/components/form/DefaultForm.js +99 -79
  32. package/src/react/components/help/DefaultTooltip.js +48 -12
  33. package/src/react/components/help/DefaultTooltip.styles.js +20 -0
  34. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  35. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  36. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  37. package/src/react/components/inputs/DefaultInput.js +296 -141
  38. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  39. package/src/react/components/inputs/DefaultSelect.js +134 -123
  40. package/src/react/components/inputs/defaultInputUtils.js +262 -213
  41. package/src/react/components/map/DefaultMap.native.js +73 -73
  42. package/src/react/components/map/DefaultMap.shared.js +911 -762
  43. package/src/react/components/map/DefaultMap.web.js +124 -68
  44. package/src/react/components/table/DefaultColumnMenu.js +22 -3
  45. package/src/react/components/table/DefaultDebug.js +44 -37
  46. package/src/react/components/table/DefaultSearchModal.js +9 -1
  47. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  48. package/src/react/components/table/DefaultTable.js +217 -111
  49. package/src/react/components/table/DefaultTable.styles.js +369 -310
  50. package/src/react/components/table/DefaultTable.utils.js +38 -4
  51. package/src/react/components/table/DefaultTableBody.js +13 -8
  52. package/src/react/components/table/DefaultTableCards.js +20 -1
  53. package/src/react/components/table/DefaultTableControls.js +180 -205
  54. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  55. package/src/react/components/table/DefaultTableInput.js +1 -0
  56. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  57. package/src/react/components/table/DefaultTableRows.js +357 -325
  58. package/src/react/components/table/DefaultTableSearch.js +28 -5
  59. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  60. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  61. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  62. package/src/react/components/upload/DefaultUpload.js +406 -0
  63. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  64. package/src/react/components/upload/DefaultUploadAttachmentsList.js +146 -0
  65. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  66. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  67. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  68. package/src/react/components/upload/fileUpload.js +93 -0
  69. package/src/react/hooks/usePostalCodeLookup.js +159 -0
  70. package/src/react/index.js +6 -0
  71. package/src/react/services/addressFormUtils.js +262 -0
  72. package/src/react/services/addressGeo.js +230 -0
  73. package/src/react/utils/tableVisibleColumnsPreferences.js +113 -15
  74. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  75. package/src/tests/react/components/files/DefaultFile.test.js +22 -6
  76. package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
  77. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  78. package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
  79. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  80. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  81. package/src/tests/react/components/table/DefaultTable.test.js +53 -2
  82. package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
  83. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  84. package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
  85. package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
  86. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  87. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  88. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  89. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +104 -0
  90. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  91. package/src/tests/react/filters/filterValue.test.js +46 -0
  92. package/src/tests/react/hooks/usePostalCodeLookup.test.js +107 -0
  93. package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
  94. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  95. package/src/tests/react/services/addressFormUtils.test.js +179 -0
  96. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  97. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  98. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  99. package/src/tests/react/services/smoke-637/index.html +132 -0
  100. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +96 -49
@@ -0,0 +1,65 @@
1
+ const EMPTY_SHORTCUTS = new Set(['', 'all', 'none', '*']);
2
+
3
+ const normalizeText = value => String(value ?? '').trim();
4
+
5
+ export const isFilledFilterValue = value => {
6
+ if (value === null || value === undefined) {
7
+ return false;
8
+ }
9
+
10
+ if (Array.isArray(value)) {
11
+ return value.some(isFilledFilterValue);
12
+ }
13
+
14
+ if (typeof value === 'object') {
15
+ const shortcut = normalizeText(value.shortcut).toLowerCase();
16
+ const range = value.customRange && typeof value.customRange === 'object'
17
+ ? value.customRange
18
+ : null;
19
+ const rangeFrom = normalizeText(
20
+ range?.from ?? value.from ?? value.start ?? value.after,
21
+ );
22
+ const rangeTo = normalizeText(
23
+ range?.to ?? value.to ?? value.end ?? value.before,
24
+ );
25
+
26
+ if ('shortcut' in value || range || 'start' in value || 'end' in value || 'after' in value || 'before' in value) {
27
+ if (EMPTY_SHORTCUTS.has(shortcut) && !rangeFrom && !rangeTo) {
28
+ return false;
29
+ }
30
+ if ((shortcut === 'custom' || !shortcut) && !rangeFrom && !rangeTo) {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ return Object.values(value).some(isFilledFilterValue);
36
+ }
37
+
38
+ return normalizeText(value) !== '';
39
+ };
40
+
41
+ export const countActiveFilters = (filters = {}) => {
42
+ if (!filters || typeof filters !== 'object' || Array.isArray(filters)) {
43
+ return 0;
44
+ }
45
+
46
+ return Object.keys(filters).filter(key => {
47
+ if (['page', 'itemsPerPage', 'order', 'sort', 'pagination'].includes(key)) {
48
+ return false;
49
+ }
50
+ return isFilledFilterValue(filters[key]);
51
+ }).length;
52
+ };
53
+
54
+ export const omitEmptyFilters = (filters = {}) => {
55
+ if (!filters || typeof filters !== 'object' || Array.isArray(filters)) {
56
+ return {};
57
+ }
58
+
59
+ return Object.keys(filters).reduce((accumulator, key) => {
60
+ if (isFilledFilterValue(filters[key])) {
61
+ accumulator[key] = filters[key];
62
+ }
63
+ return accumulator;
64
+ }, {});
65
+ };
@@ -1,17 +1,17 @@
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,
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,
8
8
  getColumnKey,
9
9
  isEditableColumn,
10
10
  normalizeId,
11
- normalizeText,
12
- resolveEditValue,
13
- } from '../inputs/defaultInputUtils';
14
- import { createStyles } from './DefaultForm.styles';
11
+ normalizeText,
12
+ resolveEditValue,
13
+ } from '../inputs/defaultInputUtils';
14
+ import { createStyles } from './DefaultForm.styles';
15
15
 
16
16
  const hasValue = value => {
17
17
  if (value === null || value === undefined) return false;
@@ -46,50 +46,67 @@ const shouldIncludeColumn = (column, row, mode) => {
46
46
  return column?.visibleForm !== false;
47
47
  };
48
48
 
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,
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 resolveCreatePayloadColumns = (columns, formColumns) => {
58
+ const formColumnKeys = new Set(formColumns.map(column => getColumnKey(column)));
59
+
60
+ return [
61
+ ...formColumns,
62
+ ...columns.filter(column => {
63
+ const fieldName = getColumnKey(column);
64
+ return fieldName && column?.createPayload === true && !formColumnKeys.has(fieldName);
65
+ }),
66
+ ];
67
+ };
68
+
69
+ const EMPTY_ROW = {};
70
+
71
+ const DefaultForm = ({
72
+ actions = {},
73
+ columns = [],
74
+ getOptionsForColumn = null,
63
75
  mode = 'edit',
64
76
  onBeforeOpen = null,
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],
77
+ onCancel = null,
78
+ onSaved = null,
79
+ row = EMPTY_ROW,
80
+ storeName = '',
81
+ }) => {
82
+ const themeStore = useStore('theme');
83
+ const { colors: themeColors } = themeStore.getters;
84
+ const palette = useMemo(
85
+ () => ({
86
+ buttonBackground: themeColors.buttonBackground,
87
+ buttonBackgroundSecondary: themeColors.buttonBackgroundSecondary,
88
+ buttonBorderSecondary: themeColors.buttonBorderSecondary,
89
+ buttonDisabledBackground: themeColors.buttonDisabledBackground,
90
+ buttonDisabledText: themeColors.buttonDisabledText,
91
+ buttonText: themeColors.buttonText,
92
+ buttonTextSecondary: themeColors.buttonTextSecondary,
93
+ dividerBorder: themeColors.dividerBorder,
94
+ textSecondary: themeColors.textSecondary,
95
+ }),
96
+ [themeColors],
97
+ );
98
+ const styles = useMemo(() => createStyles(palette), [palette]);
99
+ const formColumns = useMemo(
100
+ () => columns.filter(column => shouldIncludeColumn(column, row, mode)),
101
+ [columns, mode, row],
102
+ );
103
+ const createPayloadColumns = useMemo(
104
+ () => resolveCreatePayloadColumns(columns, formColumns),
105
+ [columns, formColumns],
90
106
  );
91
107
  const [draft, setDraft] = useState(() => buildDraft(formColumns, row));
92
108
  const [isSaving, setIsSaving] = useState(false);
109
+ const formRow = useMemo(() => ({ ...row, ...draft }), [draft, row]);
93
110
 
94
111
  useEffect(() => {
95
112
  setDraft(buildDraft(formColumns, row));
@@ -106,20 +123,23 @@ const DefaultForm = ({
106
123
  ? {}
107
124
  : { id: normalizeId(row?.['@id'] || row?.id) };
108
125
 
109
- formColumns.forEach(column => {
126
+ const payloadColumns = isCreate ? createPayloadColumns : formColumns;
127
+
128
+ payloadColumns.forEach(column => {
110
129
  const fieldName = getColumnKey(column);
111
- const nextValue = draft[fieldName];
130
+ const defaultValue = resolveDefaultValue(column, row);
131
+ const nextValue = draft[fieldName] ?? resolveEditValue(column, row, defaultValue);
112
132
 
113
133
  if (isCreate) {
114
134
  if (hasValue(nextValue)) {
115
- payload[fieldName] = formatSaveValue(column, nextValue, row);
135
+ payload[fieldName] = formatSaveValue(column, nextValue, formRow);
116
136
  }
117
137
  return;
118
138
  }
119
139
 
120
140
  const currentValue = resolveEditValue(column, row);
121
141
  if (normalizeText(currentValue) !== normalizeText(nextValue)) {
122
- payload[fieldName] = formatSaveValue(column, nextValue, row);
142
+ payload[fieldName] = formatSaveValue(column, nextValue, formRow);
123
143
  }
124
144
  });
125
145
 
@@ -143,7 +163,7 @@ const DefaultForm = ({
143
163
  .finally(() => {
144
164
  setIsSaving(false);
145
165
  });
146
- }, [actions, draft, formColumns, mode, onCancel, onSaved, row]);
166
+ }, [actions, createPayloadColumns, draft, formColumns, formRow, mode, onCancel, onSaved, row]);
147
167
 
148
168
  return (
149
169
  <>
@@ -152,7 +172,7 @@ const DefaultForm = ({
152
172
  <View style={styles.formGrid}>
153
173
  {formColumns.map(column => {
154
174
  const fieldName = getColumnKey(column);
155
- const label = formatStoreColumnLabel({
175
+ const label = column?.formLabel || formatStoreColumnLabel({
156
176
  columns,
157
177
  fieldName,
158
178
  fallbackLabel: column?.label || fieldName,
@@ -161,18 +181,18 @@ const DefaultForm = ({
161
181
 
162
182
  return (
163
183
  <View key={fieldName} style={styles.formField}>
164
- <DefaultInput
165
- accentColor={themeColors.buttonBackground}
166
- autoFocus={false}
167
- autoSave={false}
168
- column={column}
184
+ <DefaultInput
185
+ accentColor={themeColors.buttonBackground}
186
+ autoFocus={false}
187
+ autoSave={false}
188
+ column={column}
169
189
  columns={columns}
170
190
  editing
171
191
  getOptionsForColumn={getOptionsForColumn}
172
192
  label={label}
173
193
  onBeforeOpen={onBeforeOpen ? () => onBeforeOpen(column) : null}
174
194
  onChangeValue={value => setDraft(prev => ({ ...prev, [fieldName]: value }))}
175
- row={row}
195
+ row={formRow}
176
196
  showLabel
177
197
  storeName={storeName}
178
198
  value={draft[fieldName]}
@@ -191,26 +211,26 @@ const DefaultForm = ({
191
211
  )}
192
212
  </ScrollView>
193
213
 
194
- <View style={styles.actions}>
195
- <TouchableOpacity style={styles.secondaryButton} onPress={onCancel}>
196
- <Text style={styles.secondaryButtonText}>
214
+ <View style={styles.actions}>
215
+ <TouchableOpacity style={styles.secondaryButton} onPress={onCancel}>
216
+ <Text style={styles.secondaryButtonText}>
197
217
  {global.t?.t(storeName, 'button', 'cancel') || 'Cancelar'}
198
218
  </Text>
199
219
  </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>
220
+ <TouchableOpacity
221
+ style={[
222
+ styles.primaryButton,
223
+ { backgroundColor: palette.buttonBackground },
224
+ isSaving ? styles.primaryButtonDisabled : null,
225
+ ]}
226
+ activeOpacity={0.86}
227
+ disabled={isSaving}
228
+ onPress={save}
229
+ >
230
+ <Text style={[styles.primaryButtonText, isSaving ? styles.primaryButtonTextDisabled : null]}>
231
+ {isSaving ? 'Salvando' : global.t?.t(storeName, 'button', 'save') || 'Salvar'}
232
+ </Text>
233
+ </TouchableOpacity>
214
234
  </View>
215
235
  </>
216
236
  );
@@ -4,21 +4,32 @@ import styles from './DefaultTooltip.styles';
4
4
 
5
5
  const DefaultTooltip = ({
6
6
  accentColor = '#0EA5E9',
7
+ accessibilityLabel = 'Abrir ajuda',
8
+ backdropTestID,
9
+ children = null,
10
+ closeAccessibilityLabel = 'Fechar ajuda',
11
+ closeLabel = 'Fechar',
12
+ dialogTestID,
13
+ hitSlop,
7
14
  label = '?',
8
15
  message = '',
16
+ rows = [],
9
17
  style = null,
18
+ testID,
10
19
  textStyle = null,
11
20
  title = 'Ajuda',
12
21
  }) => {
13
22
  const [visible, setVisible] = useState(false);
23
+ const hasRows = Array.isArray(rows) && rows.length > 0;
24
+ const hasContent = Boolean(message || children || hasRows);
14
25
 
15
26
  const openTooltip = useCallback(() => {
16
- if (!message) {
27
+ if (!hasContent) {
17
28
  return;
18
29
  }
19
30
 
20
31
  setVisible(true);
21
- }, [message]);
32
+ }, [hasContent]);
22
33
 
23
34
  const closeTooltip = useCallback(() => {
24
35
  setVisible(false);
@@ -27,9 +38,11 @@ const DefaultTooltip = ({
27
38
  return (
28
39
  <>
29
40
  <TouchableOpacity
41
+ accessibilityLabel={accessibilityLabel}
30
42
  accessibilityRole="button"
31
43
  activeOpacity={0.85}
32
- disabled={!message}
44
+ disabled={!hasContent}
45
+ hitSlop={hitSlop}
33
46
  onPress={openTooltip}
34
47
  style={[
35
48
  styles.button,
@@ -37,12 +50,17 @@ const DefaultTooltip = ({
37
50
  borderColor: accentColor,
38
51
  backgroundColor: `${accentColor}14`,
39
52
  },
40
- !message && {opacity: 0.55},
53
+ !hasContent && {opacity: 0.55},
41
54
  style,
42
- ]}>
43
- <Text style={[styles.label, {color: accentColor}, textStyle]}>
44
- {label}
45
- </Text>
55
+ ]}
56
+ testID={testID}>
57
+ {React.isValidElement(label) ? (
58
+ label
59
+ ) : (
60
+ <Text style={[styles.label, {color: accentColor}, textStyle]}>
61
+ {label}
62
+ </Text>
63
+ )}
46
64
  </TouchableOpacity>
47
65
 
48
66
  <Modal
@@ -50,18 +68,25 @@ const DefaultTooltip = ({
50
68
  onRequestClose={closeTooltip}
51
69
  transparent
52
70
  visible={visible}>
53
- <Pressable style={styles.overlay} onPress={closeTooltip}>
71
+ <Pressable
72
+ style={styles.overlay}
73
+ onPress={closeTooltip}
74
+ testID={backdropTestID}>
54
75
  <View
76
+ accessibilityLabel={title}
77
+ accessibilityRole="summary"
55
78
  onStartShouldSetResponder={() => true}
56
79
  style={[
57
80
  styles.card,
58
81
  {
59
82
  borderColor: accentColor,
60
83
  },
61
- ]}>
84
+ ]}
85
+ testID={dialogTestID}>
62
86
  <View style={styles.cardHeader}>
63
87
  <Text style={[styles.title, {color: accentColor}]}>{title}</Text>
64
88
  <TouchableOpacity
89
+ accessibilityLabel={closeAccessibilityLabel}
65
90
  accessibilityRole="button"
66
91
  activeOpacity={0.8}
67
92
  onPress={closeTooltip}
@@ -72,11 +97,22 @@ const DefaultTooltip = ({
72
97
  },
73
98
  ]}>
74
99
  <Text style={[styles.closeButtonText, {color: accentColor}]}>
75
- Fechar
100
+ {closeLabel}
76
101
  </Text>
77
102
  </TouchableOpacity>
78
103
  </View>
79
- <Text style={styles.message}>{message}</Text>
104
+ {message ? <Text style={styles.message}>{message}</Text> : null}
105
+ {hasRows
106
+ ? rows.map(row => (
107
+ <View key={row.key || row.label} style={styles.row}>
108
+ <Text style={styles.rowLabel}>{row.label}</Text>
109
+ <Text style={styles.rowValue}>
110
+ {String(row.value ?? 'Não configurado')}
111
+ </Text>
112
+ </View>
113
+ ))
114
+ : null}
115
+ {children}
80
116
  </View>
81
117
  </Pressable>
82
118
  </Modal>
@@ -56,6 +56,26 @@ const styles = StyleSheet.create({
56
56
  fontSize: 12,
57
57
  fontWeight: '700',
58
58
  },
59
+ row: {
60
+ borderTopColor: '#E2E8F0',
61
+ borderTopWidth: 1,
62
+ flexDirection: 'row',
63
+ justifyContent: 'space-between',
64
+ paddingVertical: 9,
65
+ },
66
+ rowLabel: {
67
+ color: '#64748B',
68
+ fontSize: 13,
69
+ fontWeight: '600',
70
+ },
71
+ rowValue: {
72
+ color: '#0F172A',
73
+ flexShrink: 1,
74
+ fontSize: 13,
75
+ fontWeight: '700',
76
+ marginLeft: 12,
77
+ textAlign: 'right',
78
+ },
59
79
  });
60
80
 
61
81
  export default styles;
@@ -0,0 +1,153 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { Text, TextInput, TouchableOpacity, View } from 'react-native';
3
+ import { useStore } from '@store';
4
+ import Icon from 'react-native-vector-icons/Feather';
5
+ import {
6
+ getColumnKey,
7
+ normalizeText,
8
+ resolveColorToken,
9
+ resolveDefaultColorValue,
10
+ } from './defaultInputUtils';
11
+ import styles from './DefaultInput.styles';
12
+
13
+ const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
14
+
15
+ const normalizeColorText = value => {
16
+ const normalized = normalizeText(value);
17
+ if (!normalized) return '';
18
+
19
+ return normalized.startsWith('#') || normalized.startsWith('$')
20
+ ? normalized
21
+ : `#${normalized}`;
22
+ };
23
+
24
+ const isValidHexColor = value => HEX_COLOR_REGEX.test(resolveColorToken(value));
25
+
26
+ const DefaultColorInput = ({
27
+ accentColor = '#2563EB',
28
+ autoFocus = true,
29
+ autoSave = true,
30
+ column,
31
+ containerStyle = null,
32
+ defaultColor: defaultColorProp = '',
33
+ editing = false,
34
+ inputStyle = null,
35
+ label = '',
36
+ onCancelEditing = null,
37
+ onChangeValue = null,
38
+ onSave = null,
39
+ onStartEditing = null,
40
+ row = {},
41
+ saving = false,
42
+ showLabel = false,
43
+ value,
44
+ variant = 'cell',
45
+ }) => {
46
+ const themeColors = useStore('theme')?.getters?.colors || {};
47
+ const fieldName = getColumnKey(column);
48
+ const rawValue = value ?? row?.[fieldName];
49
+ const defaultColor = useMemo(
50
+ () => resolveDefaultColorValue(column, themeColors, defaultColorProp),
51
+ [column, defaultColorProp, themeColors],
52
+ );
53
+ const readValue = normalizeColorText(rawValue) || defaultColor || '#CBD5E1';
54
+ const previewColor = resolveColorToken(readValue, themeColors) || defaultColor || '#CBD5E1';
55
+ const [draftValue, setDraftValue] = useState(readValue);
56
+ const isForm = variant === 'form';
57
+
58
+ useEffect(() => {
59
+ setDraftValue(readValue);
60
+ }, [readValue, editing]);
61
+
62
+ const saveDraft = () => {
63
+ const nextValue = normalizeColorText(draftValue) || defaultColor || '#CBD5E1';
64
+ if (autoSave) {
65
+ onSave?.(nextValue);
66
+ return;
67
+ }
68
+
69
+ onChangeValue?.(nextValue);
70
+ };
71
+
72
+ const updateDraft = nextValue => {
73
+ setDraftValue(nextValue);
74
+ if (!autoSave) onChangeValue?.(normalizeColorText(nextValue));
75
+ };
76
+
77
+ const applyDefaultColor = () => {
78
+ if (!defaultColor) return;
79
+ setDraftValue(defaultColor);
80
+ if (!autoSave) onChangeValue?.(defaultColor);
81
+ };
82
+
83
+ if (!editing && !isForm) {
84
+ return (
85
+ <View style={[styles.wrap, containerStyle]}>
86
+ {showLabel ? <Text style={styles.fieldLabel}>{label || column?.label || fieldName}</Text> : null}
87
+ <TouchableOpacity
88
+ style={styles.readButton}
89
+ activeOpacity={0.78}
90
+ disabled={typeof onStartEditing !== 'function'}
91
+ onPress={() => onStartEditing?.()}
92
+ >
93
+ <View style={[styles.colorPreview, { backgroundColor: previewColor }]} />
94
+ <Text style={styles.readText} numberOfLines={1}>
95
+ {readValue}
96
+ </Text>
97
+ {saving ? (
98
+ <Text style={[styles.savingText, { color: accentColor }]}>Salvando</Text>
99
+ ) : (
100
+ <Icon style={styles.editIcon} name="edit-2" size={13} color="#64748B" />
101
+ )}
102
+ </TouchableOpacity>
103
+ </View>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <View style={[styles.wrap, containerStyle]}>
109
+ {showLabel ? <Text style={styles.fieldLabel}>{label || column?.label || fieldName}</Text> : null}
110
+ <View style={styles.editingWrap}>
111
+ <View style={styles.editingRow}>
112
+ <View style={[styles.colorPreview, { backgroundColor: resolveColorToken(draftValue, themeColors) || previewColor }]} />
113
+ <TextInput
114
+ autoCapitalize="characters"
115
+ autoFocus={autoFocus && !isForm}
116
+ onBlur={autoSave ? saveDraft : undefined}
117
+ onChangeText={updateDraft}
118
+ onSubmitEditing={saveDraft}
119
+ placeholder={defaultColor || '#CBD5E1'}
120
+ placeholderTextColor="#CBD5E1"
121
+ selectTextOnFocus
122
+ style={[
123
+ styles.input,
124
+ styles.colorInput,
125
+ isForm ? styles.formInput : null,
126
+ isValidHexColor(draftValue) ? null : styles.invalidColorInput,
127
+ inputStyle,
128
+ ]}
129
+ value={draftValue}
130
+ />
131
+ {defaultColor ? (
132
+ <TouchableOpacity
133
+ accessibilityRole="button"
134
+ activeOpacity={0.8}
135
+ onPress={applyDefaultColor}
136
+ style={styles.colorDefaultButton}
137
+ >
138
+ <Icon name="rotate-ccw" size={13} color="#64748B" />
139
+ </TouchableOpacity>
140
+ ) : null}
141
+ {!isForm ? (
142
+ <TouchableOpacity style={styles.cancelButton} activeOpacity={0.8} onPress={onCancelEditing}>
143
+ <Icon name="x" size={14} color="#64748B" />
144
+ </TouchableOpacity>
145
+ ) : null}
146
+ </View>
147
+ {saving ? <Text style={styles.savingText}>Salvando</Text> : null}
148
+ </View>
149
+ </View>
150
+ );
151
+ };
152
+
153
+ export default DefaultColorInput;