@dashadmin/dash-components 1.3.24 → 1.3.26

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 (111) hide show
  1. package/dist/components/AuditLog/AuditLog.js +345 -1
  2. package/dist/components/AuditLog/index.js +4 -1
  3. package/dist/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.js +630 -3
  4. package/dist/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.js +53 -1
  5. package/dist/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.js +647 -3
  6. package/dist/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.js +53 -1
  7. package/dist/components/Json/Json.js +235 -1
  8. package/dist/components/JsonColorSelector/JsonColorSelector.js +1172 -1
  9. package/dist/components/JsonColorSelector/JsonColorSelectorEnhanced.js +863 -2
  10. package/dist/components/JsonColorSelector/JsonCssVarValues.js +738 -1
  11. package/dist/components/JsonColorSelector/components/ColorEditDialog.js +426 -1
  12. package/dist/components/JsonColorSelector/components/ColorPaletteItem.js +75 -1
  13. package/dist/components/JsonColorSelector/components/ImageColorExtractor.js +283 -1
  14. package/dist/components/JsonColorSelector/helpers/functions.js +101 -1
  15. package/dist/components/JsonColorSelector/useAsyncColorThief.js +105 -1
  16. package/dist/components/ListActive/ListActive.js +126 -1
  17. package/dist/components/ListActive/ListBoolean.js +124 -1
  18. package/dist/components/NotificationPreferences/NotificationPreferences.js +317 -1
  19. package/dist/components/NotificationPreferences/index.js +7 -1
  20. package/dist/components/SearchableSelects/RASearchableSelect.js +236 -1
  21. package/dist/components/SearchableSelects/RASearchableSelectCheckboxes.js +479 -1
  22. package/dist/components/SearchableSelects/RASearchableSelectChips.js +465 -1
  23. package/dist/components/SearchableSelects/RASearchableSelectChipsRecordContext.js +295 -1
  24. package/dist/components/SearchableSelects/SearchableSelect.js +202 -1
  25. package/dist/components/SearchableSelects/SearchableSelectCheckboxes.js +290 -1
  26. package/dist/components/SortableDataGrid/SortableDataGrid.js +72 -1
  27. package/dist/components/SortableDataGrid/SortableDataGridBody.js +167 -1
  28. package/dist/components/SortableDataGrid/SortableDataGridHeader.js +109 -1
  29. package/dist/components/SortableDataGrid/SortableDataGridRow.js +283 -1
  30. package/dist/components/Upload/SingleImageUploader.js +40 -1
  31. package/dist/components/custom/PackageCopyMethod.js +78 -1
  32. package/dist/components/custom/PackageRadioButtonGroup.js +45 -1
  33. package/dist/components/dialog/Basic.js +8 -1
  34. package/dist/components/dialog/Correct.js +14 -1
  35. package/dist/components/dialog/Error.js +14 -1
  36. package/dist/components/dialog/Info.js +14 -1
  37. package/dist/components/dialog/InfoV2.js +21 -1
  38. package/dist/components/dialog/NotFound.js +14 -1
  39. package/dist/components/dialog/QuickSearch.js +117 -1
  40. package/dist/components/notifications/NotificationsCenter.js +597 -1
  41. package/dist/components/notifications/index.js +24 -1
  42. package/dist/components/tables/PaginationComponent.js +36 -1
  43. package/dist/components/theme/AppLoadingFallback.js +38 -2
  44. package/dist/components/theme/ErrorBoundary.js +52 -1
  45. package/dist/components/theme/InitialLoader.js +13 -1
  46. package/dist/components/theme/NotFound.js +136 -1
  47. package/dist/components/theme/NotResults.js +38 -1
  48. package/dist/hooks/useDraggable.js +6 -1
  49. package/dist/hooks/useNotifications.js +8 -1
  50. package/dist/hooks/useQuickSearch.js +26 -1
  51. package/dist/index.js +66 -5567
  52. package/dist/utils/setNativeValue.js +17 -1
  53. package/package.json +149 -132
  54. package/src/components/AuditLog/AuditLog.tsx +476 -0
  55. package/src/components/AuditLog/index.ts +2 -0
  56. package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +917 -0
  57. package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +39 -0
  58. package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +916 -0
  59. package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +44 -0
  60. package/src/components/Json/Json.tsx +312 -0
  61. package/src/components/JsonColorSelector/JsonColorSelector.tsx +1435 -0
  62. package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +1029 -0
  63. package/src/components/JsonColorSelector/JsonCssVarValues.tsx +863 -0
  64. package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +491 -0
  65. package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +80 -0
  66. package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +328 -0
  67. package/src/components/JsonColorSelector/helpers/functions.tsx +123 -0
  68. package/src/components/JsonColorSelector/interfaces/interfaces.tsx +22 -0
  69. package/src/components/JsonColorSelector/useAsyncColorThief.tsx +97 -0
  70. package/src/components/ListActive/ListActive.tsx +106 -0
  71. package/src/components/ListActive/ListBoolean.tsx +96 -0
  72. package/src/components/NotificationPreferences/NotificationPreferences.tsx +463 -0
  73. package/src/components/NotificationPreferences/index.tsx +2 -0
  74. package/src/components/SearchableSelects/RASearchableSelect.tsx +300 -0
  75. package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +604 -0
  76. package/src/components/SearchableSelects/RASearchableSelectChips.tsx +558 -0
  77. package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +362 -0
  78. package/src/components/SearchableSelects/SearchableSelect.tsx +321 -0
  79. package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +355 -0
  80. package/src/components/SortableDataGrid/SortableDataGrid.tsx +56 -0
  81. package/src/components/SortableDataGrid/SortableDataGridBody.tsx +175 -0
  82. package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +171 -0
  83. package/src/components/SortableDataGrid/SortableDataGridRow.tsx +247 -0
  84. package/src/components/Upload/SingleImageUploader.tsx +55 -0
  85. package/src/components/custom/PackageCopyMethod.tsx +109 -0
  86. package/src/components/custom/PackageRadioButtonGroup.tsx +47 -0
  87. package/src/components/dialog/Basic.tsx +11 -0
  88. package/src/components/dialog/Correct.tsx +16 -0
  89. package/src/components/dialog/Error.tsx +15 -0
  90. package/src/components/dialog/Info.tsx +15 -0
  91. package/src/components/dialog/InfoV2.tsx +35 -0
  92. package/src/components/dialog/NotFound.tsx +18 -0
  93. package/src/components/dialog/QuickSearch.tsx +115 -0
  94. package/src/components/notifications/NotificationsCenter.tsx +792 -0
  95. package/src/components/notifications/index.ts +42 -0
  96. package/src/components/tables/PaginationComponent.tsx +17 -0
  97. package/src/components/theme/AppLoadingFallback.tsx +42 -0
  98. package/src/components/theme/ErrorBoundary.tsx +57 -0
  99. package/src/components/theme/InitialLoader.tsx +16 -0
  100. package/src/components/theme/NotFound.tsx +124 -0
  101. package/src/components/theme/NotResults.tsx +54 -0
  102. package/src/hooks/useDraggable.ts +11 -0
  103. package/src/hooks/useNotifications.ts +14 -0
  104. package/src/hooks/useQuickSearch.tsx +29 -0
  105. package/src/index.tsx +77 -0
  106. package/src/utils/setNativeValue.tsx +23 -0
  107. package/dist/color-thief-CTwGFhOB.js +0 -272
  108. /package/{dist → src}/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -0
  109. /package/{dist → src}/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -0
  110. /package/{dist → src}/components/SortableDataGrid/README.md +0 -0
  111. /package/{dist → src}/styles/index.less +0 -0
@@ -0,0 +1,96 @@
1
+ import React, { act, useCallback, useEffect, useState } from 'react';
2
+ import { IDashAutoAdminCustomFieldComponent } from 'dash-auto-admin';
3
+ /*import { ICommunes } from '@app/interfaces/ICommunes'; */
4
+ import { useRecordContext, useUpdate } from 'react-admin';
5
+ import { Switch, Box } from '@mui/material';
6
+ import { FormControlLabel } from '@mui/material';
7
+ import { AttributeToField } from 'dash-auto-admin/src/mui/AttributeToField';
8
+ import AttributeToInput from 'dash-auto-admin/src/mui/AttributeToInput';
9
+
10
+ export interface IListActive extends IDashAutoAdminCustomFieldComponent {
11
+ activeLabel?: string;
12
+ inactiveLabel?: string;
13
+ enableLabel?: string;
14
+ disableLabel?: string;
15
+ }
16
+
17
+ const ListActive = ({
18
+ method,
19
+ attribute,
20
+ resourceConfig,
21
+ activeLabel = 'Activo',
22
+ inactiveLabel = 'Inactivo',
23
+ enableLabel = 'Activar',
24
+ disableLabel = 'Desactivar',
25
+ }: IListActive) => {
26
+ const [ ,setLoading] = useState<boolean>(true);
27
+ const [update, { isLoading: updateLoading }] =
28
+ useUpdate();
29
+ const record: any = useRecordContext();
30
+ const onError = (error: any) => {
31
+ console.error(error);
32
+ };
33
+
34
+ const onChange = useCallback(
35
+ async (value: boolean) => {
36
+ window.dispatchEvent(
37
+ new MessageEvent('dash-global-loader', { data: true })
38
+ );
39
+
40
+ try {
41
+ await update(
42
+ resourceConfig.model + '/change-status/' + record.id,
43
+ {
44
+ id: record.id,
45
+ data: { is_active: value },
46
+ previousData: record,
47
+ },
48
+ {
49
+ onSuccess: () => {
50
+ // Force a refresh of the record context
51
+ record.is_active = value;
52
+ },
53
+ onSettled: (data, error) => {
54
+ if (error) onError(error);
55
+ window.dispatchEvent(
56
+ new MessageEvent('dash-global-loader', { data: false })
57
+ );
58
+ },
59
+ }
60
+ );
61
+ } catch (error) {
62
+ onError(error);
63
+ }
64
+ },
65
+ [update, record]
66
+ );
67
+
68
+ useEffect(() => {
69
+ setLoading(false);
70
+ }, [record]);
71
+ const is_active = record?.is_active ? true : false;
72
+ const textSwitch = record?.is_active ? disableLabel : enableLabel;
73
+ switch (method) {
74
+ case 'create':
75
+ case 'edit':
76
+ return AttributeToInput(method, resourceConfig, {...attribute,custom:false,type:Boolean})
77
+ case 'view':
78
+ return AttributeToField(method, resourceConfig, {...attribute,custom:false,type:Boolean})
79
+ case 'list':
80
+ return (
81
+ <div>
82
+ <Box>
83
+ <Switch
84
+
85
+ onChange={(e, value) => onChange(value)}
86
+ {...(is_active === true && { defaultChecked: true })}
87
+ />{is_active ? activeLabel : inactiveLabel}
88
+ </Box>
89
+ </div>
90
+ )
91
+ default:
92
+ return <>{attribute.label}: {is_active ? activeLabel : inactiveLabel}</>;
93
+ }
94
+ };
95
+
96
+ export default ListActive;
@@ -0,0 +1,463 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ Switch,
8
+ FormControlLabel,
9
+ Alert,
10
+ TextField,
11
+ InputAdornment,
12
+ IconButton,
13
+ Chip,
14
+ } from '@mui/material';
15
+ import { Search as SearchIcon, Clear as ClearIcon, Email as EmailIcon, Notifications as PushIcon } from '@mui/icons-material';
16
+ import { useRecordContext } from 'react-admin';
17
+ import { useFormContext } from 'react-hook-form';
18
+ import { AuthPersistenceService } from 'dash-auth';
19
+ import { IDashAutoAdminCustomFieldComponent } from 'dash-auto-admin';
20
+
21
+ interface NotificationConfig {
22
+ id: string;
23
+ name: string;
24
+ className: string;
25
+ hasEmail: boolean;
26
+ hasPush: boolean;
27
+ hasSocket: boolean;
28
+ hasDatabase: boolean;
29
+ }
30
+
31
+ interface NotificationPreference {
32
+ id: string;
33
+ name: string;
34
+ email: boolean;
35
+ push: boolean;
36
+ }
37
+
38
+ // Helper functions - moved outside component
39
+ const getNestedValue = (obj: any, path: string): any => {
40
+ if (!path || !obj) return [];
41
+ const keys = path.split('.');
42
+ return keys.reduce((o, key) => (o && o[key] !== undefined ? o[key] : []), obj);
43
+ };
44
+
45
+ const setNestedValue = (obj: any, path: string, value: any): any => {
46
+ if (!path) return value;
47
+ const keys = path.split('.');
48
+ const lastKey = keys.pop();
49
+ const result = { ...obj };
50
+ let current = result;
51
+
52
+ for (const key of keys) {
53
+ if (!current[key]) current[key] = {};
54
+ current = current[key];
55
+ }
56
+
57
+ if (lastKey) {
58
+ current[lastKey] = value;
59
+ }
60
+ return result;
61
+ };
62
+
63
+ /**
64
+ * NotificationPreferencesEdit - Edit mode component
65
+ * All hooks are called unconditionally at the top level
66
+ */
67
+ export const NotificationPreferencesEdit: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
68
+ const { attribute } = props;
69
+
70
+ // ALL HOOKS CALLED UNCONDITIONALLY AT TOP LEVEL
71
+ const record = useRecordContext();
72
+ const formContext = useFormContext();
73
+ const [searchTerm, setSearchTerm] = React.useState<string>('');
74
+ const [localPreferences, setLocalPreferences] = React.useState<NotificationPreference[] | null>(null);
75
+
76
+ // Get form methods safely
77
+ const setValue = formContext?.setValue;
78
+ const getValues = formContext?.getValues;
79
+
80
+ // Calculate attribute paths
81
+ const attributePath = attribute?.attribute || '';
82
+ const isNestedSetting = attributePath.startsWith('preferences.');
83
+ const preferencesPath = isNestedSetting ? attributePath.split('.').slice(1).join('.') : attributePath;
84
+
85
+ // Get available notifications from systemValues (memoized)
86
+ const availableNotifications = useMemo<NotificationConfig[]>(() => {
87
+ const systemValues = AuthPersistenceService.getSystemValues();
88
+ console.log('🔔 NotificationPreferencesEdit - systemValues:', systemValues);
89
+ const notifications = systemValues?.user_notifications || [];
90
+ console.log('🔔 NotificationPreferencesEdit - user_notifications:', notifications);
91
+ return notifications;
92
+ }, []);
93
+
94
+ // Get current preferences from record/form (memoized)
95
+ const initialPreferences = useMemo<NotificationPreference[]>(() => {
96
+ let currentPrefs: NotificationPreference[] = [];
97
+
98
+ if (record) {
99
+ const recordValue = isNestedSetting
100
+ ? getNestedValue(record, preferencesPath)
101
+ : record[attributePath];
102
+ currentPrefs = Array.isArray(recordValue) ? recordValue : [];
103
+ console.log('🔔 NotificationPreferencesEdit - preferences from record:', currentPrefs);
104
+ } else if (getValues) {
105
+ const formValues = getValues();
106
+ const formValue = isNestedSetting
107
+ ? getNestedValue(formValues, preferencesPath)
108
+ : formValues[attributePath];
109
+ currentPrefs = Array.isArray(formValue) ? formValue : [];
110
+ console.log('🔔 NotificationPreferencesEdit - preferences from form:', currentPrefs);
111
+ }
112
+
113
+ // Map available notifications to preferences with defaults
114
+ return availableNotifications.map(notification => {
115
+ const existing = currentPrefs.find(p => p.id === notification.className);
116
+ return existing || {
117
+ id: notification.className,
118
+ name: notification.name,
119
+ email: notification.hasEmail,
120
+ push: notification.hasPush,
121
+ };
122
+ });
123
+ }, [availableNotifications, record, attributePath, isNestedSetting, preferencesPath, getValues]);
124
+
125
+ // Use local state if modified, otherwise use initial
126
+ const preferences = localPreferences ?? initialPreferences;
127
+
128
+ // Filter preferences based on search term
129
+ const filteredPreferences = useMemo(() => {
130
+ if (!searchTerm.trim()) return preferences;
131
+ return preferences.filter(pref =>
132
+ pref.name.toLowerCase().includes(searchTerm.toLowerCase())
133
+ );
134
+ }, [preferences, searchTerm]);
135
+
136
+ // Update form value when preferences change
137
+ const updateFormValue = (newPreferences: NotificationPreference[]) => {
138
+ if (!setValue) return;
139
+
140
+ if (isNestedSetting) {
141
+ const formValues = getValues?.() || {};
142
+ const updatedValues = setNestedValue({ ...formValues }, preferencesPath, newPreferences);
143
+ setValue(attributePath, getNestedValue(updatedValues, preferencesPath), { shouldDirty: true });
144
+ } else {
145
+ setValue(attributePath, newPreferences, { shouldDirty: true });
146
+ }
147
+ };
148
+
149
+ // Handle toggle
150
+ const handleToggle = (notificationId: string, channel: 'email' | 'push') => {
151
+ const updatedPreferences = preferences.map(pref =>
152
+ pref.id === notificationId
153
+ ? { ...pref, [channel]: !pref[channel] }
154
+ : pref
155
+ );
156
+ setLocalPreferences(updatedPreferences);
157
+ updateFormValue(updatedPreferences);
158
+ };
159
+
160
+ // Early return if no form context
161
+ if (!formContext) {
162
+ return (
163
+ <Alert severity="warning">
164
+ Form context not available. This component must be used within a form.
165
+ </Alert>
166
+ );
167
+ }
168
+
169
+ // Early return if no notifications
170
+ if (availableNotifications.length === 0) {
171
+ return (
172
+ <Alert severity="info">
173
+ No notifications available to configure.
174
+ <Typography variant="caption" display="block" sx={{ mt: 1 }}>
175
+ Debug: No user_notifications found in systemValues.
176
+ Try logging out and logging back in to refresh auth data.
177
+ </Typography>
178
+ </Alert>
179
+ );
180
+ }
181
+
182
+ return (
183
+ <Box sx={{ mt: 1, mb: 2 }}>
184
+ <Typography variant="h6" gutterBottom>
185
+ Notification Preferences
186
+ </Typography>
187
+ <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
188
+ Configure which notifications you want to receive via email and push notifications
189
+ </Typography>
190
+
191
+ {/* Search Box */}
192
+ {preferences.length > 5 && (
193
+ <TextField
194
+ fullWidth
195
+ variant="outlined"
196
+ placeholder="Search notifications..."
197
+ value={searchTerm}
198
+ onChange={(e) => setSearchTerm(e.target.value)}
199
+ sx={{ mb: 2 }}
200
+ InputProps={{
201
+ startAdornment: (
202
+ <InputAdornment position="start">
203
+ <SearchIcon />
204
+ </InputAdornment>
205
+ ),
206
+ endAdornment: searchTerm && (
207
+ <InputAdornment position="end">
208
+ <IconButton onClick={() => setSearchTerm('')} edge="end" size="small">
209
+ <ClearIcon />
210
+ </IconButton>
211
+ </InputAdornment>
212
+ ),
213
+ }}
214
+ />
215
+ )}
216
+
217
+ {/* No results message */}
218
+ {searchTerm && filteredPreferences.length === 0 && (
219
+ <Alert severity="info" sx={{ mb: 2 }}>
220
+ No notifications found matching "{searchTerm}"
221
+ </Alert>
222
+ )}
223
+
224
+ {/* Notification list */}
225
+ {filteredPreferences.map((preference) => {
226
+ const notification = availableNotifications.find(n => n.className === preference.id);
227
+ if (!notification) return null;
228
+
229
+ return (
230
+ <Card
231
+ key={preference.id}
232
+ variant="outlined"
233
+ sx={{
234
+ mb: 1,
235
+ '&:hover': {
236
+ boxShadow: 2,
237
+ },
238
+ }}
239
+ >
240
+ <CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
241
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
242
+ <Box sx={{ flex: 1 }}>
243
+ <Typography variant="subtitle1" fontWeight="bold">
244
+ {preference.name}
245
+ </Typography>
246
+ <Box sx={{ display: 'flex', gap: 1, mt: 0.5 }}>
247
+ {notification.hasSocket && (
248
+ <Chip label="Real-time" size="small" color="info" />
249
+ )}
250
+ {notification.hasDatabase && (
251
+ <Chip label="Stored" size="small" color="default" />
252
+ )}
253
+ </Box>
254
+ </Box>
255
+ <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
256
+ {notification.hasEmail && (
257
+ <FormControlLabel
258
+ control={
259
+ <Switch
260
+ checked={preference.email}
261
+ onChange={() => handleToggle(preference.id, 'email')}
262
+ color="primary"
263
+ />
264
+ }
265
+ label={
266
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
267
+ <EmailIcon fontSize="small" />
268
+ <Typography variant="body2">Email</Typography>
269
+ </Box>
270
+ }
271
+ />
272
+ )}
273
+ {notification.hasPush && (
274
+ <FormControlLabel
275
+ control={
276
+ <Switch
277
+ checked={preference.push}
278
+ onChange={() => handleToggle(preference.id, 'push')}
279
+ color="secondary"
280
+ />
281
+ }
282
+ label={
283
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
284
+ <PushIcon fontSize="small" />
285
+ <Typography variant="body2">Push</Typography>
286
+ </Box>
287
+ }
288
+ />
289
+ )}
290
+ </Box>
291
+ </Box>
292
+ </CardContent>
293
+ </Card>
294
+ );
295
+ })}
296
+ </Box>
297
+ );
298
+ };
299
+
300
+ /**
301
+ * NotificationPreferencesView - View mode component
302
+ * All hooks are called unconditionally at the top level
303
+ */
304
+ export const NotificationPreferencesView: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
305
+ const { attribute } = props;
306
+
307
+ // ALL HOOKS CALLED UNCONDITIONALLY AT TOP LEVEL
308
+ const record = useRecordContext();
309
+ const [searchTerm, setSearchTerm] = React.useState<string>('');
310
+
311
+ // Calculate attribute paths
312
+ const attributePath = attribute?.attribute || '';
313
+ const isNestedSetting = attributePath.startsWith('preferences.');
314
+ const preferencesPath = isNestedSetting ? attributePath.split('.').slice(1).join('.') : attributePath;
315
+
316
+ // Get available notifications from systemValues (memoized)
317
+ const availableNotifications = useMemo<NotificationConfig[]>(() => {
318
+ const systemValues = AuthPersistenceService.getSystemValues();
319
+ return systemValues?.user_notifications || [];
320
+ }, []);
321
+
322
+ // Get current preferences from record (memoized)
323
+ const preferences = useMemo<NotificationPreference[]>(() => {
324
+ if (!record) return [];
325
+ const recordValue = isNestedSetting
326
+ ? getNestedValue(record, preferencesPath)
327
+ : record[attributePath];
328
+ return Array.isArray(recordValue) ? recordValue : [];
329
+ }, [record, attributePath, isNestedSetting, preferencesPath]);
330
+
331
+ // Filter preferences based on search term
332
+ const filteredPreferences = useMemo(() => {
333
+ if (!searchTerm.trim()) return preferences;
334
+ return preferences.filter(pref =>
335
+ pref.name.toLowerCase().includes(searchTerm.toLowerCase())
336
+ );
337
+ }, [preferences, searchTerm]);
338
+
339
+ // Early return if no notifications
340
+ if (availableNotifications.length === 0) {
341
+ return (
342
+ <Alert severity="info">
343
+ No notification preferences configured.
344
+ </Alert>
345
+ );
346
+ }
347
+
348
+ return (
349
+ <Box sx={{ mt: 1, mb: 2 }}>
350
+ <Typography variant="h6" gutterBottom>
351
+ Notification Preferences
352
+ </Typography>
353
+
354
+ {/* Search Box */}
355
+ {preferences.length > 5 && (
356
+ <TextField
357
+ fullWidth
358
+ variant="outlined"
359
+ placeholder="Search notifications..."
360
+ value={searchTerm}
361
+ onChange={(e) => setSearchTerm(e.target.value)}
362
+ sx={{ mb: 2 }}
363
+ InputProps={{
364
+ startAdornment: (
365
+ <InputAdornment position="start">
366
+ <SearchIcon />
367
+ </InputAdornment>
368
+ ),
369
+ endAdornment: searchTerm && (
370
+ <InputAdornment position="end">
371
+ <IconButton onClick={() => setSearchTerm('')} edge="end" size="small">
372
+ <ClearIcon />
373
+ </IconButton>
374
+ </InputAdornment>
375
+ ),
376
+ }}
377
+ />
378
+ )}
379
+
380
+ {/* Notification list */}
381
+ {filteredPreferences.map((preference) => {
382
+ const notification = availableNotifications.find(n => n.className === preference.id);
383
+ if (!notification) return null;
384
+
385
+ return (
386
+ <Card key={preference.id} variant="outlined" sx={{ mb: 1 }}>
387
+ <CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
388
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
389
+ <Box sx={{ flex: 1 }}>
390
+ <Typography variant="subtitle1" fontWeight="bold">
391
+ {preference.name}
392
+ </Typography>
393
+ <Box sx={{ display: 'flex', gap: 1, mt: 0.5 }}>
394
+ {notification.hasSocket && (
395
+ <Chip label="Real-time" size="small" color="info" />
396
+ )}
397
+ {notification.hasDatabase && (
398
+ <Chip label="Stored" size="small" color="default" />
399
+ )}
400
+ </Box>
401
+ </Box>
402
+ <Box sx={{ display: 'flex', gap: 2 }}>
403
+ {notification.hasEmail && (
404
+ <Chip
405
+ icon={<EmailIcon />}
406
+ label="Email"
407
+ color={preference.email ? 'primary' : 'default'}
408
+ variant={preference.email ? 'filled' : 'outlined'}
409
+ />
410
+ )}
411
+ {notification.hasPush && (
412
+ <Chip
413
+ icon={<PushIcon />}
414
+ label="Push"
415
+ color={preference.push ? 'secondary' : 'default'}
416
+ variant={preference.push ? 'filled' : 'outlined'}
417
+ />
418
+ )}
419
+ </Box>
420
+ </Box>
421
+ </CardContent>
422
+ </Card>
423
+ );
424
+ })}
425
+
426
+ {preferences.length === 0 && (
427
+ <Alert severity="info">
428
+ No notification preferences configured
429
+ </Alert>
430
+ )}
431
+ </Box>
432
+ );
433
+ };
434
+
435
+ /**
436
+ * NotificationPreferences - Main component
437
+ * NO CONDITIONAL HOOKS - All hooks called at top level
438
+ * Renders appropriate view based on method prop
439
+ */
440
+ const NotificationPreferences: React.FC<IDashAutoAdminCustomFieldComponent> = (props) => {
441
+
442
+
443
+
444
+ const { method="edit", attribute=null, resourceConfig=null } = props;
445
+
446
+ console.log('🔔 NotificationPreferences - method:', method);
447
+ console.log('🔔 NotificationPreferences - attribute:', attribute);
448
+
449
+ // Determine which component to render based on method
450
+ // No hooks, no conditional logic, just a simple switch
451
+ switch (method) {
452
+ case 'edit':
453
+ case 'create':
454
+ return <><NotificationPreferencesEdit method={method} attribute={attribute} resourceConfig={resourceConfig} /></>;
455
+ case 'view':
456
+ return <><NotificationPreferencesView method={method} attribute={attribute} resourceConfig={resourceConfig} /></>;
457
+ default:
458
+ console.log('🔔 NotificationPreferences - unsupported method:', method);
459
+ return null;
460
+ }
461
+ };
462
+
463
+ export default NotificationPreferences;
@@ -0,0 +1,2 @@
1
+ export { default as NotificationPreferences } from './NotificationPreferences';
2
+ export { NotificationPreferencesEdit, NotificationPreferencesView } from './NotificationPreferences';