@controleonline/ui-common 1.2.98 → 1.2.100
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/docs/homepage-1056.md +12 -0
- package/package.json +1 -1
- package/src/api/index.js +40 -54
- package/src/react/components/AnimatedModal.js +6 -1
- package/src/react/components/BottomNavigationBar.js +19 -8
- package/src/react/components/BottomNavigationBar.soft.styles.js +29 -0
- package/src/react/components/DefaultProvider.styles.js +4 -0
- package/src/react/components/DefaultProvider.web.js +6 -3
- package/src/react/components/ProductCatalogCacheService.js +53 -45
- package/src/react/components/RuntimeBottomNavigationBar.js +12 -3
- package/src/react/components/RuntimeInfoFooter.home.spacing.js +5 -0
- package/src/react/components/RuntimeInfoFooter.js +8 -2
- package/src/react/components/UserAvatar.js +5 -14
- package/src/react/hooks/useBottomNavigationPreference.js +49 -0
- package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +6 -4
- package/src/react/pages/Devices/deviceTypes/shared.js +9 -4
- package/src/react/pages/Devices.js +184 -183
- package/src/react/pages/IntegrationConfigPage.js +59 -13
- package/src/react/pages/IntegrationConfigPage.utils.js +17 -0
- package/src/react/pages/integrationsCatalog.js +71 -0
- package/src/react/utils/productCatalogCacheRevision.js +13 -0
- package/src/tests/react/utils/runtimeFooter.test.js +189 -189
- package/src/tests/react/utils/storedSessionToken.test.js +22 -0
- package/src/utils/integrationConfigs.js +23 -0
- package/src/utils/storedSessionToken.js +18 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Homepage ADMIN/MANAGER — app-community#1056
|
|
2
|
+
|
|
3
|
+
Soft appearance is opt-in for HomePage only. Existing menu routes/parameters, permissions, stores and translations are preserved. App-provided homepage colors and components are used only in this appearance. Default consumers retain their current behavior.
|
|
4
|
+
|
|
5
|
+
Source: https://github.com/ControleOnline/app-community/issues/1056
|
|
6
|
+
Local evidence: 40 homepage tests and 34 runtime footer/zoom tests passed. No production authorization.
|
|
7
|
+
|
|
8
|
+
## Web login translation bootstrap
|
|
9
|
+
|
|
10
|
+
Keep the navigation tree mounted while translations initialize. Hide the shell and disable pointer/accessibility interaction until the existing translation readiness contract succeeds. Unmounting navigation during login resets its route and repeatedly mounts/registers the bottom bar, causing React maximum update depth. No route, permission, translation source, theme or zoom changes. Native behavior is unchanged.
|
|
11
|
+
|
|
12
|
+
Regression evidence: app-community homepage bootstrapNavigation.test.js reproduced maximum update depth before the fix, then passed after it. 44 tests across 9 homepage suites passed. Runtime Dev reproduced the unfixed bug; new runtime verification is required after consuming ui-common 1.2.95.
|
package/package.json
CHANGED
package/src/api/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {readStoredSessionToken} from '@controleonline/ui-common/src/utils/storedSessionToken';
|
|
1
2
|
import myFetch from '@controleonline/ui-common/src/api/fetch';
|
|
2
3
|
import axios from 'axios';
|
|
3
4
|
import {env as APP_ENV} from '@env';
|
|
@@ -215,8 +216,8 @@ async function requestSmokeUrl(url, options) {
|
|
|
215
216
|
export const api = {
|
|
216
217
|
device: JSON.parse(localStorage.getItem('device') || '{}'),
|
|
217
218
|
masterDevice: JSON.parse(localStorage.getItem('master-device') || '{}'),
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
|
|
220
|
+
|
|
220
221
|
upload: async function (uri, formData) {
|
|
221
222
|
const token = await this.getToken();
|
|
222
223
|
const appDomain = resolveAppDomain(APP_ENV.DOMAIN);
|
|
@@ -242,8 +243,8 @@ export const api = {
|
|
|
242
243
|
fetch: async function (uri, options = {}) {
|
|
243
244
|
if (typeof options.headers === 'undefined')
|
|
244
245
|
Object.assign(options, { headers: new Headers() });
|
|
245
|
-
|
|
246
|
-
// Always refresh device from localStorage in case it was set after module load
|
|
246
|
+
|
|
247
|
+
// Always refresh device from localStorage in case it was set after module load
|
|
247
248
|
try {
|
|
248
249
|
this.device = JSON.parse(localStorage.getItem('device') || '{}');
|
|
249
250
|
} catch (e) {
|
|
@@ -270,16 +271,16 @@ export const api = {
|
|
|
270
271
|
if (options.body) {
|
|
271
272
|
if (typeof options.body === 'object') {
|
|
272
273
|
bodyDeviceId = options.body.device || null;
|
|
273
|
-
} else if (typeof options.body === 'string') {
|
|
274
|
-
try {
|
|
275
|
-
const parsedBody = JSON.parse(options.body);
|
|
276
|
-
bodyDeviceId = parsedBody?.device || null;
|
|
277
|
-
} catch (e) {
|
|
278
|
-
bodyDeviceId = null;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
274
|
+
} else if (typeof options.body === 'string') {
|
|
275
|
+
try {
|
|
276
|
+
const parsedBody = JSON.parse(options.body);
|
|
277
|
+
bodyDeviceId = parsedBody?.device || null;
|
|
278
|
+
} catch (e) {
|
|
279
|
+
bodyDeviceId = null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
283
284
|
const requestDevice = resolveRequestDevice(
|
|
284
285
|
this.masterDevice,
|
|
285
286
|
this.device,
|
|
@@ -296,10 +297,10 @@ export const api = {
|
|
|
296
297
|
options.headers.set('DEVICE-TYPE', requestDeviceType);
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
|
-
if (options.responseType != 'text') {
|
|
300
|
-
options.headers.set('Content-Type', MIME_TYPE);
|
|
301
|
-
options.headers.set('Accept', MIME_TYPE);
|
|
302
|
-
}
|
|
300
|
+
if (options.responseType != 'text') {
|
|
301
|
+
options.headers.set('Content-Type', MIME_TYPE);
|
|
302
|
+
options.headers.set('Accept', MIME_TYPE);
|
|
303
|
+
}
|
|
303
304
|
options.headers.set('App-Domain', resolveAppDomain(APP_ENV.DOMAIN));
|
|
304
305
|
|
|
305
306
|
if (options.body && typeof options.body != 'string') {
|
|
@@ -310,28 +311,13 @@ export const api = {
|
|
|
310
311
|
if (options.params) {
|
|
311
312
|
uri = this.buildQueryString(uri, options);
|
|
312
313
|
}
|
|
313
|
-
return myFetch(uri, options).catch(e => {
|
|
314
|
-
throw e;
|
|
315
|
-
});
|
|
316
|
-
},
|
|
317
|
-
async getToken() {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (sessionString) {
|
|
321
|
-
try {
|
|
322
|
-
const cleanString =
|
|
323
|
-
typeof sessionString == 'string' &&
|
|
324
|
-
sessionString.startsWith('__q_objt|')
|
|
325
|
-
? sessionString.substring('__q_objt|'.length)
|
|
326
|
-
: sessionString;
|
|
327
|
-
session = JSON.parse(cleanString); // Transforma a string em objeto
|
|
328
|
-
} catch (e) {
|
|
329
|
-
console.error('Failed to parse session from localStorage', e);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return session?.token || session?.api_key;
|
|
334
|
-
},
|
|
314
|
+
return myFetch(uri, options).catch(e => {
|
|
315
|
+
throw e;
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
async getToken() {
|
|
319
|
+
return readStoredSessionToken();
|
|
320
|
+
},
|
|
335
321
|
serialize(obj, prefix = '') {
|
|
336
322
|
const pairs = [];
|
|
337
323
|
|
|
@@ -344,12 +330,12 @@ export const api = {
|
|
|
344
330
|
|
|
345
331
|
const value = obj[key];
|
|
346
332
|
let fullKey = prefix ? `${prefix}.${key}` : key; // Usa ponto para objetos
|
|
347
|
-
|
|
348
|
-
if (value === null || value === undefined) {
|
|
349
|
-
pairs.push(`${fullKey}=`);
|
|
350
|
-
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
351
|
-
// Objeto aninhado: chama serialize recursivamente com ponto
|
|
352
|
-
pairs.push(...this.serialize(value, fullKey));
|
|
333
|
+
|
|
334
|
+
if (value === null || value === undefined) {
|
|
335
|
+
pairs.push(`${fullKey}=`);
|
|
336
|
+
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
337
|
+
// Objeto aninhado: chama serialize recursivamente com ponto
|
|
338
|
+
pairs.push(...this.serialize(value, fullKey));
|
|
353
339
|
} else if (Array.isArray(value)) {
|
|
354
340
|
// Array: adiciona [] e itera sobre os valores
|
|
355
341
|
if (value.length === 0) {
|
|
@@ -364,14 +350,14 @@ export const api = {
|
|
|
364
350
|
});
|
|
365
351
|
}
|
|
366
352
|
} else {
|
|
367
|
-
// Valor simples
|
|
368
|
-
pairs.push(`${fullKey}=${encodeURIComponent(value)}`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return pairs;
|
|
373
|
-
},
|
|
374
|
-
|
|
353
|
+
// Valor simples
|
|
354
|
+
pairs.push(`${fullKey}=${encodeURIComponent(value)}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return pairs;
|
|
359
|
+
},
|
|
360
|
+
|
|
375
361
|
buildQueryString(uri, options) {
|
|
376
362
|
const safeUri = stripDomainQueryParams(uri);
|
|
377
363
|
|
|
@@ -38,6 +38,8 @@ export default function AnimatedModal({
|
|
|
38
38
|
onRequestClose,
|
|
39
39
|
children,
|
|
40
40
|
style,
|
|
41
|
+
overlayColor,
|
|
42
|
+
contentStyle,
|
|
41
43
|
}) {
|
|
42
44
|
const [showing, setShowing] = useState(visible);
|
|
43
45
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
@@ -125,6 +127,8 @@ export default function AnimatedModal({
|
|
|
125
127
|
const overlayStyle = [
|
|
126
128
|
styles.overlay,
|
|
127
129
|
style,
|
|
130
|
+
overlayColor,
|
|
131
|
+
contentStyle,
|
|
128
132
|
Platform.OS === 'android' ? {paddingBottom: keyboardHeight} : null,
|
|
129
133
|
];
|
|
130
134
|
|
|
@@ -152,11 +156,12 @@ export default function AnimatedModal({
|
|
|
152
156
|
style={[
|
|
153
157
|
StyleSheet.absoluteFillObject,
|
|
154
158
|
styles.overlayBg,
|
|
159
|
+
overlayColor ? {backgroundColor: overlayColor} : null,
|
|
155
160
|
{opacity: overlayOpacity},
|
|
156
161
|
]}
|
|
157
162
|
/>
|
|
158
163
|
<Animated.View
|
|
159
|
-
style={[styles.content, {transform: [{translateY: slideY}]}]}>
|
|
164
|
+
style={[styles.content, contentStyle, {transform: [{translateY: slideY}]}]}>
|
|
160
165
|
{normalizedChildren}
|
|
161
166
|
</Animated.View>
|
|
162
167
|
<Toasts
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React, {useLayoutEffect, useMemo} from 'react';
|
|
2
2
|
import {Pressable, Text, View} from 'react-native';
|
|
3
|
-
import
|
|
3
|
+
import LegacyIcon from 'react-native-vector-icons/Feather';
|
|
4
|
+
import HomeIcon from '@controleonline/../../src/components/HomeIcon';
|
|
4
5
|
import {useTheme} from './DefaultProvider';
|
|
5
6
|
import RuntimeInfoFooter from './RuntimeInfoFooter';
|
|
6
7
|
import {resolveMenuRouteParams} from '../utils/menuNavigation';
|
|
7
8
|
import createStyles from './BottomNavigationBar.styles';
|
|
9
|
+
import softenStyles from './BottomNavigationBar.soft.styles';
|
|
10
|
+
import {resolveHomepagePalette} from '@controleonline/../../src/styles/homepagePalette';
|
|
8
11
|
|
|
9
12
|
const BottomNavigationBar = ({
|
|
10
13
|
navigation,
|
|
@@ -14,7 +17,11 @@ const BottomNavigationBar = ({
|
|
|
14
17
|
colors = {},
|
|
15
18
|
testID = 'bottom-navigation',
|
|
16
19
|
useModernWebChromeProps = false,
|
|
20
|
+
appearance = 'soft',
|
|
21
|
+
placement = 'overlay',
|
|
17
22
|
}) => {
|
|
23
|
+
const Icon = appearance === 'soft' ? HomeIcon : LegacyIcon;
|
|
24
|
+
colors = appearance === 'soft' ? resolveHomepagePalette(colors) : colors;
|
|
18
25
|
const theme = useTheme?.() || {};
|
|
19
26
|
const runtimeFooter = theme?.runtimeFooter || null;
|
|
20
27
|
const registerBottomNavigation =
|
|
@@ -31,14 +38,14 @@ const BottomNavigationBar = ({
|
|
|
31
38
|
|
|
32
39
|
const styles = useMemo(
|
|
33
40
|
() =>
|
|
34
|
-
createStyles({
|
|
41
|
+
softenStyles(createStyles({
|
|
35
42
|
activeBackground,
|
|
36
43
|
dockBackground,
|
|
37
44
|
dockBorder,
|
|
38
45
|
dockShadow,
|
|
39
46
|
activeBorder,
|
|
40
47
|
useModernWebChromeProps,
|
|
41
|
-
}),
|
|
48
|
+
}), colors, appearance),
|
|
42
49
|
[
|
|
43
50
|
activeBackground,
|
|
44
51
|
activeBorder,
|
|
@@ -46,6 +53,7 @@ const BottomNavigationBar = ({
|
|
|
46
53
|
dockBorder,
|
|
47
54
|
dockShadow,
|
|
48
55
|
useModernWebChromeProps,
|
|
56
|
+
appearance,
|
|
49
57
|
],
|
|
50
58
|
);
|
|
51
59
|
|
|
@@ -97,9 +105,7 @@ const BottomNavigationBar = ({
|
|
|
97
105
|
|
|
98
106
|
const routeItems = Array.isArray(items) ? items : [];
|
|
99
107
|
const knownRoute = routeItems.some(item => item?.route === activeRouteName);
|
|
100
|
-
const effectiveActiveRoute = knownRoute
|
|
101
|
-
? activeRouteName
|
|
102
|
-
: routeItems[0]?.route || '';
|
|
108
|
+
const effectiveActiveRoute = knownRoute ? activeRouteName : '';
|
|
103
109
|
|
|
104
110
|
const navigateTo = item => {
|
|
105
111
|
try {
|
|
@@ -116,15 +122,18 @@ const BottomNavigationBar = ({
|
|
|
116
122
|
? [styles.host, styles.hostPointerEventsBoxNone]
|
|
117
123
|
: styles.host;
|
|
118
124
|
const footerProps = {
|
|
125
|
+
appearance,
|
|
119
126
|
appVersion: runtimeFooter?.appVersion,
|
|
120
|
-
colors:
|
|
127
|
+
colors: appearance === 'soft'
|
|
128
|
+
? {...runtimeFooter?.colors, footerBackground: colors.footerBackground, footerText: colors.footerText}
|
|
129
|
+
: runtimeFooter?.colors || {},
|
|
121
130
|
mainCompany: runtimeFooter?.mainCompany,
|
|
122
131
|
device: runtimeFooter?.device,
|
|
123
132
|
useModernWebChromeProps,
|
|
124
133
|
};
|
|
125
134
|
|
|
126
135
|
return (
|
|
127
|
-
<View {...hostProps} style={hostStyle}>
|
|
136
|
+
<View {...hostProps} style={[hostStyle, placement === 'inline' ? {position: 'relative', flexShrink: 0, zIndex: 0} : null]}>
|
|
128
137
|
<View style={styles.stack}>
|
|
129
138
|
<View style={styles.dock} testID={testID}>
|
|
130
139
|
<View style={styles.itemsRow}>
|
|
@@ -141,6 +150,8 @@ const BottomNavigationBar = ({
|
|
|
141
150
|
<Pressable
|
|
142
151
|
key={item.route}
|
|
143
152
|
accessibilityRole="button"
|
|
153
|
+
accessibilityLabel={item.label}
|
|
154
|
+
accessibilityState={{selected: isActive, disabled: Boolean(isDisabled)}}
|
|
144
155
|
disabled={isDisabled}
|
|
145
156
|
onPress={() => navigateTo(item)}
|
|
146
157
|
style={({pressed}) => [
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {Platform} from 'react-native';
|
|
2
|
+
|
|
3
|
+
// Shared compact chrome on every route; explicit legacy consumers can opt out.
|
|
4
|
+
export default (styles, colors, appearance) => appearance !== 'soft' ? styles : {
|
|
5
|
+
...styles,
|
|
6
|
+
dock: [styles.dock, {
|
|
7
|
+
paddingHorizontal: 0,
|
|
8
|
+
paddingTop: 0,
|
|
9
|
+
paddingBottom: 0,
|
|
10
|
+
borderWidth: 0,
|
|
11
|
+
borderTopWidth: 1,
|
|
12
|
+
borderColor: colors.navigationBorder,
|
|
13
|
+
borderTopLeftRadius: 0,
|
|
14
|
+
borderTopRightRadius: 0,
|
|
15
|
+
elevation: 0,
|
|
16
|
+
shadowOpacity: 0,
|
|
17
|
+
...(Platform.OS === 'web' ? {boxShadow: 'none'} : {}),
|
|
18
|
+
}],
|
|
19
|
+
itemsRow: [styles.itemsRow, {minHeight: 56, paddingHorizontal: 8, paddingVertical: 4}],
|
|
20
|
+
item: [styles.item, {minHeight: 46, paddingVertical: 3, borderRadius: 10}],
|
|
21
|
+
iconWrap: [styles.iconWrap, {width: 22, height: 22, marginBottom: 2}],
|
|
22
|
+
itemLabel: [styles.itemLabel, {fontSize: 11, lineHeight: 14, fontWeight: '400'}],
|
|
23
|
+
footerSlot: [styles.footerSlot, {
|
|
24
|
+
borderTopWidth: 1,
|
|
25
|
+
borderColor: colors.navigationBorder,
|
|
26
|
+
paddingHorizontal: 0,
|
|
27
|
+
minHeight: 28,
|
|
28
|
+
}],
|
|
29
|
+
};
|
|
@@ -395,15 +395,18 @@ export const DefaultProvider = ({
|
|
|
395
395
|
|
|
396
396
|
useWebThemePalette({resolveDomainThemeColors, baseThemeColors, mainCompany, resolveThemePalette, runtimeColors, applyPaletteToRuntimeColors, applyThemeCssVariables, actions, currentCompany, isShopClientApp});
|
|
397
397
|
|
|
398
|
-
|
|
399
|
-
return <View style={providerStyles.loadingContainer} />;
|
|
400
|
-
}
|
|
398
|
+
const translationPending = requiresTranslateBootstrap && !isTranslateBootstrapReady;
|
|
401
399
|
|
|
402
400
|
return (
|
|
403
401
|
<ThemeContext.Provider value={themeContextValue}>
|
|
404
402
|
<View
|
|
403
|
+
pointerEvents={translationPending ? 'none' : 'auto'}
|
|
404
|
+
aria-hidden={translationPending}
|
|
405
405
|
style={[
|
|
406
406
|
providerStyles.shell,
|
|
407
|
+
// Keep navigation mounted while translations load. Unmounting it
|
|
408
|
+
// resets the login route and repeatedly registers the bottom bar.
|
|
409
|
+
translationPending && providerStyles.translationPending,
|
|
407
410
|
{
|
|
408
411
|
backgroundColor: colors?.background || runtimeColors.background,
|
|
409
412
|
},
|
|
@@ -1,45 +1,53 @@
|
|
|
1
|
-
import React, {useEffect} from 'react';
|
|
2
|
-
import {useStore} from '@store';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
messages
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
categoryActions.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
import React, {useEffect} from 'react';
|
|
2
|
+
import {useStore} from '@store';
|
|
3
|
+
import {invalidateProductCatalogCache} from '../utils/productCatalogCacheRevision';
|
|
4
|
+
|
|
5
|
+
const ProductCatalogCacheService = () => {
|
|
6
|
+
const categoriesStore = useStore('categories');
|
|
7
|
+
const productsStore = useStore('products');
|
|
8
|
+
|
|
9
|
+
const categoryActions = categoriesStore.actions;
|
|
10
|
+
const productActions = productsStore.actions;
|
|
11
|
+
|
|
12
|
+
const {messages, message} = categoriesStore.getters;
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (
|
|
16
|
+
messages &&
|
|
17
|
+
messages.length > 0 &&
|
|
18
|
+
(!message || Object.keys(message).length === 0)
|
|
19
|
+
) {
|
|
20
|
+
const nextMessages = [...messages];
|
|
21
|
+
categoryActions.setMessage(nextMessages.pop());
|
|
22
|
+
categoryActions.setMessages(nextMessages);
|
|
23
|
+
}
|
|
24
|
+
}, [categoryActions, message, messages]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!message || Object.keys(message).length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (message?.command !== 'clear-product-cache') {
|
|
32
|
+
categoryActions.setMessage(null);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
invalidateProductCatalogCache();
|
|
37
|
+
for (const context of ['products', 'supplies']) {
|
|
38
|
+
localStorage.removeItem(`categories:${context}`);
|
|
39
|
+
localStorage.removeItem(`categories-company:${context}`);
|
|
40
|
+
}
|
|
41
|
+
localStorage.setItem('categories', JSON.stringify([]));
|
|
42
|
+
localStorage.removeItem('categories-company');
|
|
43
|
+
categoryActions.setItems([]);
|
|
44
|
+
productActions.setItems([]);
|
|
45
|
+
categoryActions.setReload?.(true);
|
|
46
|
+
productActions.setReload?.(true);
|
|
47
|
+
categoryActions.setMessage(null);
|
|
48
|
+
}, [categoryActions, message, productActions]);
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default ProductCatalogCacheService;
|
|
@@ -19,6 +19,8 @@ const {
|
|
|
19
19
|
} = require('@controleonline/ui-common/src/react/utils/bottomNavigationToolbar');
|
|
20
20
|
|
|
21
21
|
const RuntimeBottomNavigationBar = ({
|
|
22
|
+
appearance = 'soft',
|
|
23
|
+
placement = 'overlay',
|
|
22
24
|
activeRouteName: activeRouteNameProp,
|
|
23
25
|
colors: colorsOverride,
|
|
24
26
|
disabled: disabledOverride,
|
|
@@ -41,13 +43,16 @@ const RuntimeBottomNavigationBar = ({
|
|
|
41
43
|
const peopleGetters = peopleStore?.getters || {};
|
|
42
44
|
const currentCompany = peopleGetters.currentCompany || {};
|
|
43
45
|
const themeColors = themeGetters.colors || {};
|
|
44
|
-
const [
|
|
46
|
+
const [menuState, setMenuState] = useState({scope: '', menus: []});
|
|
45
47
|
const preset = presetKey ? getBottomNavigationPreset(presetKey) : null;
|
|
46
48
|
const routeAliases = routeAliasesProp || preset?.routeAliases || {};
|
|
47
49
|
const appType = normalizeAppType(app_type);
|
|
50
|
+
const menuScope = JSON.stringify([appType, currentCompany?.id, menuType, presetKey]);
|
|
51
|
+
const runtimeMenus = menuState.scope === menuScope ? menuState.menus : [];
|
|
48
52
|
|
|
49
53
|
// Always consult menus-people for role-aware toolbar; preset is fallback only.
|
|
50
54
|
useEffect(() => {
|
|
55
|
+
const setRuntimeMenus = menus => setMenuState({scope: menuScope, menus});
|
|
51
56
|
if (!currentCompany?.id || !menuType) {
|
|
52
57
|
setRuntimeMenus(
|
|
53
58
|
resolveToolbarRuntimeMenus({
|
|
@@ -104,7 +109,7 @@ const RuntimeBottomNavigationBar = ({
|
|
|
104
109
|
return () => {
|
|
105
110
|
cancelled = true;
|
|
106
111
|
};
|
|
107
|
-
}, [appType, currentCompany?.id, menuType, preset, presetKey]);
|
|
112
|
+
}, [appType, currentCompany?.id, menuType, preset, presetKey, menuScope]);
|
|
108
113
|
|
|
109
114
|
const colors = useMemo(() => {
|
|
110
115
|
if (colorsOverride) {
|
|
@@ -150,15 +155,19 @@ const RuntimeBottomNavigationBar = ({
|
|
|
150
155
|
const knownRoute = navItems.some(item => item.route === resolvedActiveRoute);
|
|
151
156
|
const effectiveActiveRoute = knownRoute
|
|
152
157
|
? resolvedActiveRoute
|
|
153
|
-
:
|
|
158
|
+
: activeRouteName;
|
|
154
159
|
|
|
155
160
|
const isDisabled =
|
|
156
161
|
typeof disabledOverride === 'boolean'
|
|
157
162
|
? disabledOverride
|
|
158
163
|
: !currentCompany || Object.keys(currentCompany).length === 0;
|
|
159
164
|
|
|
165
|
+
if (!navItems.length) return null;
|
|
166
|
+
|
|
160
167
|
return (
|
|
161
168
|
<BottomNavigationBar
|
|
169
|
+
appearance={appearance}
|
|
170
|
+
placement={placement}
|
|
162
171
|
activeRouteName={effectiveActiveRoute}
|
|
163
172
|
colors={colors}
|
|
164
173
|
disabled={isDisabled}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '@controleonline/ui-common/src/react/utils/runtimeFooter';
|
|
23
23
|
import styles from './RuntimeInfoFooter.styles';
|
|
24
24
|
import RuntimeFooterMarqueeText from './RuntimeFooterMarqueeText';
|
|
25
|
+
import resolveHomeFooterSpacing from './RuntimeInfoFooter.home.spacing';
|
|
25
26
|
|
|
26
27
|
const ROTATION_INTERVAL_MS = 4000;
|
|
27
28
|
const COMPACT_BREAKPOINT = 720;
|
|
@@ -66,6 +67,7 @@ const RuntimeInfoFooter = ({
|
|
|
66
67
|
device,
|
|
67
68
|
colors,
|
|
68
69
|
useModernWebChromeProps = false,
|
|
70
|
+
appearance = 'legacy',
|
|
69
71
|
}) => {
|
|
70
72
|
const {width} = useWindowDimensions();
|
|
71
73
|
const insets = useSafeAreaInsets();
|
|
@@ -258,6 +260,7 @@ const RuntimeInfoFooter = ({
|
|
|
258
260
|
: inlineText
|
|
259
261
|
) || primaryText || device?.id || '';
|
|
260
262
|
const backgroundColor = colors?.footerBackground;
|
|
263
|
+
const homeSpacing = resolveHomeFooterSpacing(appearance, Platform.OS, insets.bottom);
|
|
261
264
|
const borderColor = colors?.footerBorder;
|
|
262
265
|
const textColor = resolveVisibleFooterTextColor(colors);
|
|
263
266
|
const loadingColor = colors?.footerLink || colors?.primary || textColor;
|
|
@@ -267,7 +270,9 @@ const RuntimeInfoFooter = ({
|
|
|
267
270
|
: styles.shell;
|
|
268
271
|
|
|
269
272
|
return (
|
|
270
|
-
<View {...shellProps} style={shellStyle
|
|
273
|
+
<View {...shellProps} style={[shellStyle, homeSpacing && {
|
|
274
|
+
paddingBottom: homeSpacing.safeAreaBottom, backgroundColor,
|
|
275
|
+
}]}>
|
|
271
276
|
<View
|
|
272
277
|
testID="runtime-info-footer"
|
|
273
278
|
style={[
|
|
@@ -276,7 +281,8 @@ const RuntimeInfoFooter = ({
|
|
|
276
281
|
{
|
|
277
282
|
backgroundColor,
|
|
278
283
|
borderTopColor: borderColor,
|
|
279
|
-
paddingBottom: bottomInset,
|
|
284
|
+
paddingBottom: homeSpacing ? homeSpacing.contentPadding : bottomInset,
|
|
285
|
+
...(homeSpacing && !showDebugInfo ? {paddingTop: homeSpacing.contentPadding} : {}),
|
|
280
286
|
},
|
|
281
287
|
]}>
|
|
282
288
|
<View style={styles.primaryRow}>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {readStoredSessionToken} from '../../utils/storedSessionToken';
|
|
1
2
|
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
3
|
import {Image, Platform, StyleSheet, Text, View} from 'react-native';
|
|
3
4
|
import {env as APP_ENV} from '@env';
|
|
@@ -14,18 +15,6 @@ const isBackendDownloadUrl = uri =>
|
|
|
14
15
|
/** Session cache: gravatar URL → true (ok) | false (missing). Avoids repeat probes. */
|
|
15
16
|
const gravatarProbeCache = new Map();
|
|
16
17
|
|
|
17
|
-
const readSessionToken = () => {
|
|
18
|
-
if (typeof localStorage === 'undefined' || !localStorage?.getItem) {
|
|
19
|
-
return '';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const session = JSON.parse(localStorage.getItem('session') || '{}');
|
|
24
|
-
return normalizeUrl(session?.api_key || session?.token);
|
|
25
|
-
} catch {
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
18
|
|
|
30
19
|
/**
|
|
31
20
|
* Probe Gravatar (d=404) via fetch BEFORE exposing URL to <Image>.
|
|
@@ -86,7 +75,7 @@ const useDisplayUri = uri => {
|
|
|
86
75
|
return;
|
|
87
76
|
}
|
|
88
77
|
|
|
89
|
-
const token =
|
|
78
|
+
const token = readStoredSessionToken();
|
|
90
79
|
|
|
91
80
|
// Authenticated backend download on web
|
|
92
81
|
if (isBackendDownloadUrl(next) && Platform.OS === 'web' && token) {
|
|
@@ -156,6 +145,7 @@ const useDisplayUri = uri => {
|
|
|
156
145
|
|
|
157
146
|
const UserAvatar = ({
|
|
158
147
|
imageUrl,
|
|
148
|
+
imageHeaders,
|
|
159
149
|
email,
|
|
160
150
|
name,
|
|
161
151
|
size = 40,
|
|
@@ -215,7 +205,8 @@ const UserAvatar = ({
|
|
|
215
205
|
<View style={containerStyle}>
|
|
216
206
|
{displayUri ? (
|
|
217
207
|
<Image
|
|
218
|
-
source={
|
|
208
|
+
source={imageHeaders && Platform.OS !== 'web' && displayUri === imageUrl
|
|
209
|
+
? {uri: displayUri, headers: imageHeaders} : {uri: displayUri}}
|
|
219
210
|
style={styles.image}
|
|
220
211
|
onError={() => setSourceIndex(index => index + 1)}
|
|
221
212
|
/>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {Platform} from 'react-native';
|
|
3
|
+
|
|
4
|
+
const nativeStorage = () => {
|
|
5
|
+
const module = require('@react-native-async-storage/async-storage');
|
|
6
|
+
return module.default || module;
|
|
7
|
+
};
|
|
8
|
+
const deviceStorage = {
|
|
9
|
+
getItem: key => Platform.OS === 'web' ? globalThis.localStorage?.getItem(key) : nativeStorage().getItem(key),
|
|
10
|
+
setItem: (key, value) => Platform.OS === 'web' ? globalThis.localStorage.setItem(key, value) : nativeStorage().setItem(key, value),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Visibility is personal device state. It never changes server menu permissions.
|
|
14
|
+
export default function useBottomNavigationPreference({enabled, principal, appType, storage = deviceStorage}) {
|
|
15
|
+
const key = enabled && principal && appType
|
|
16
|
+
? `co.bottom-navigation.v1:${encodeURIComponent(appType)}:${encodeURIComponent(principal)}` : '';
|
|
17
|
+
const current = useRef(key);
|
|
18
|
+
current.current = key;
|
|
19
|
+
const [state, setState] = useState({key: '', ready: false, remembered: true, session: null, error: ''});
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
current.current = key;
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
setState({key, ready: false, remembered: true, session: null, error: ''});
|
|
24
|
+
if (key) Promise.resolve().then(() => storage.getItem(key)).then(value => {
|
|
25
|
+
if (!cancelled) setState({key, ready: true, remembered: value !== 'hidden', session: null, error: ''});
|
|
26
|
+
}).catch(() => {
|
|
27
|
+
if (!cancelled) setState({key, ready: true, remembered: true, session: null, error: 'Não foi possível recuperar a preferência da barra.'});
|
|
28
|
+
});
|
|
29
|
+
return () => {cancelled = true; if (current.current === key) current.current = null;};
|
|
30
|
+
}, [key, storage]);
|
|
31
|
+
const ready = Boolean(key && state.key === key && state.ready);
|
|
32
|
+
const setSessionVisible = visible => {
|
|
33
|
+
if (ready) setState(previous => ({...previous, session: Boolean(visible), error: ''}));
|
|
34
|
+
};
|
|
35
|
+
const setRememberedVisible = async visible => {
|
|
36
|
+
if (!ready) return false;
|
|
37
|
+
try {
|
|
38
|
+
await storage.setItem(key, visible ? 'visible' : 'hidden');
|
|
39
|
+
if (current.current !== key) return false;
|
|
40
|
+
setState(previous => ({...previous, remembered: Boolean(visible), session: null, error: ''}));
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
if (current.current === key) setState(previous => ({...previous, error: 'Não foi possível lembrar a preferência neste aparelho.'}));
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return {ready, visible: ready && (state.session ?? state.remembered), rememberedVisible: ready ? state.remembered : true,
|
|
48
|
+
error: state.key === key ? state.error : '', setSessionVisible, setRememberedVisible};
|
|
49
|
+
}
|