@controleonline/ui-common 1.2.97 → 1.2.99
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/.release/rc-manifest.json +7 -7
- 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 +6 -5
- package/src/react/components/BottomNavigationBar.soft.styles.js +5 -4
- package/src/react/components/ProductCatalogCacheService.js +53 -45
- package/src/react/components/RuntimeBottomNavigationBar.js +11 -4
- package/src/react/components/UserAvatar.js +2 -13
- 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/utils/productCatalogCacheRevision.js +13 -0
- package/src/react/utils/runtimeFooter.js +24 -3
- package/src/tests/react/utils/runtimeFooter.test.js +26 -0
- package/src/tests/react/utils/storedSessionToken.test.js +22 -0
- package/src/utils/storedSessionToken.js +18 -0
- package/docs/homepage-1056.md +0 -16
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.
|
|
3
|
-
"rc":
|
|
4
|
-
"branch": "rc/1.2.
|
|
5
|
-
"baseMaster": "
|
|
6
|
-
"tasks": [
|
|
7
|
-
"repositories": { "ControleOnline/ui-common": "
|
|
8
|
-
"compositionNotes": ["
|
|
2
|
+
"version": "1.2.98",
|
|
3
|
+
"rc": 2,
|
|
4
|
+
"branch": "rc/1.2.98-rc.2",
|
|
5
|
+
"baseMaster": "aa5ad406d201d23bb88023bc4b7a7d4f9b11fd0a",
|
|
6
|
+
"tasks": [88],
|
|
7
|
+
"repositories": { "ControleOnline/ui-common": "0c96fc6ebac3af856cfba103fa0694171f590ce1" },
|
|
8
|
+
"compositionNotes": ["Task 88: rodapé mostra …últimos 2 blocos quando o identificador não é IPv4. rc.2 inclui bump de versão 1.2.98 para publicação npm; supersedes rc.1."],
|
|
9
9
|
"frozen": true
|
|
10
10
|
}
|
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
|
|
@@ -17,7 +17,8 @@ const BottomNavigationBar = ({
|
|
|
17
17
|
colors = {},
|
|
18
18
|
testID = 'bottom-navigation',
|
|
19
19
|
useModernWebChromeProps = false,
|
|
20
|
-
appearance = '
|
|
20
|
+
appearance = 'soft',
|
|
21
|
+
placement = 'overlay',
|
|
21
22
|
}) => {
|
|
22
23
|
const Icon = appearance === 'soft' ? HomeIcon : LegacyIcon;
|
|
23
24
|
colors = appearance === 'soft' ? resolveHomepagePalette(colors) : colors;
|
|
@@ -104,9 +105,7 @@ const BottomNavigationBar = ({
|
|
|
104
105
|
|
|
105
106
|
const routeItems = Array.isArray(items) ? items : [];
|
|
106
107
|
const knownRoute = routeItems.some(item => item?.route === activeRouteName);
|
|
107
|
-
const effectiveActiveRoute = knownRoute
|
|
108
|
-
? activeRouteName
|
|
109
|
-
: routeItems[0]?.route || '';
|
|
108
|
+
const effectiveActiveRoute = knownRoute ? activeRouteName : '';
|
|
110
109
|
|
|
111
110
|
const navigateTo = item => {
|
|
112
111
|
try {
|
|
@@ -134,7 +133,7 @@ const BottomNavigationBar = ({
|
|
|
134
133
|
};
|
|
135
134
|
|
|
136
135
|
return (
|
|
137
|
-
<View {...hostProps} style={hostStyle}>
|
|
136
|
+
<View {...hostProps} style={[hostStyle, placement === 'inline' ? {position: 'relative', flexShrink: 0, zIndex: 0} : null]}>
|
|
138
137
|
<View style={styles.stack}>
|
|
139
138
|
<View style={styles.dock} testID={testID}>
|
|
140
139
|
<View style={styles.itemsRow}>
|
|
@@ -151,6 +150,8 @@ const BottomNavigationBar = ({
|
|
|
151
150
|
<Pressable
|
|
152
151
|
key={item.route}
|
|
153
152
|
accessibilityRole="button"
|
|
153
|
+
accessibilityLabel={item.label}
|
|
154
|
+
accessibilityState={{selected: isActive, disabled: Boolean(isDisabled)}}
|
|
154
155
|
disabled={isDisabled}
|
|
155
156
|
onPress={() => navigateTo(item)}
|
|
156
157
|
style={({pressed}) => [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Platform} from 'react-native';
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// Shared compact chrome on every route; explicit legacy consumers can opt out.
|
|
4
4
|
export default (styles, colors, appearance) => appearance !== 'soft' ? styles : {
|
|
5
5
|
...styles,
|
|
6
6
|
dock: [styles.dock, {
|
|
@@ -16,9 +16,10 @@ export default (styles, colors, appearance) => appearance !== 'soft' ? styles :
|
|
|
16
16
|
shadowOpacity: 0,
|
|
17
17
|
...(Platform.OS === 'web' ? {boxShadow: 'none'} : {}),
|
|
18
18
|
}],
|
|
19
|
-
itemsRow: [styles.itemsRow, {minHeight:
|
|
20
|
-
item: [styles.item, {minHeight:
|
|
21
|
-
|
|
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'}],
|
|
22
23
|
footerSlot: [styles.footerSlot, {
|
|
23
24
|
borderTopWidth: 1,
|
|
24
25
|
borderColor: colors.navigationBorder,
|
|
@@ -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,7 +19,8 @@ const {
|
|
|
19
19
|
} = require('@controleonline/ui-common/src/react/utils/bottomNavigationToolbar');
|
|
20
20
|
|
|
21
21
|
const RuntimeBottomNavigationBar = ({
|
|
22
|
-
appearance = '
|
|
22
|
+
appearance = 'soft',
|
|
23
|
+
placement = 'overlay',
|
|
23
24
|
activeRouteName: activeRouteNameProp,
|
|
24
25
|
colors: colorsOverride,
|
|
25
26
|
disabled: disabledOverride,
|
|
@@ -42,13 +43,16 @@ const RuntimeBottomNavigationBar = ({
|
|
|
42
43
|
const peopleGetters = peopleStore?.getters || {};
|
|
43
44
|
const currentCompany = peopleGetters.currentCompany || {};
|
|
44
45
|
const themeColors = themeGetters.colors || {};
|
|
45
|
-
const [
|
|
46
|
+
const [menuState, setMenuState] = useState({scope: '', menus: []});
|
|
46
47
|
const preset = presetKey ? getBottomNavigationPreset(presetKey) : null;
|
|
47
48
|
const routeAliases = routeAliasesProp || preset?.routeAliases || {};
|
|
48
49
|
const appType = normalizeAppType(app_type);
|
|
50
|
+
const menuScope = JSON.stringify([appType, currentCompany?.id, menuType, presetKey]);
|
|
51
|
+
const runtimeMenus = menuState.scope === menuScope ? menuState.menus : [];
|
|
49
52
|
|
|
50
53
|
// Always consult menus-people for role-aware toolbar; preset is fallback only.
|
|
51
54
|
useEffect(() => {
|
|
55
|
+
const setRuntimeMenus = menus => setMenuState({scope: menuScope, menus});
|
|
52
56
|
if (!currentCompany?.id || !menuType) {
|
|
53
57
|
setRuntimeMenus(
|
|
54
58
|
resolveToolbarRuntimeMenus({
|
|
@@ -105,7 +109,7 @@ const RuntimeBottomNavigationBar = ({
|
|
|
105
109
|
return () => {
|
|
106
110
|
cancelled = true;
|
|
107
111
|
};
|
|
108
|
-
}, [appType, currentCompany?.id, menuType, preset, presetKey]);
|
|
112
|
+
}, [appType, currentCompany?.id, menuType, preset, presetKey, menuScope]);
|
|
109
113
|
|
|
110
114
|
const colors = useMemo(() => {
|
|
111
115
|
if (colorsOverride) {
|
|
@@ -151,16 +155,19 @@ const RuntimeBottomNavigationBar = ({
|
|
|
151
155
|
const knownRoute = navItems.some(item => item.route === resolvedActiveRoute);
|
|
152
156
|
const effectiveActiveRoute = knownRoute
|
|
153
157
|
? resolvedActiveRoute
|
|
154
|
-
:
|
|
158
|
+
: activeRouteName;
|
|
155
159
|
|
|
156
160
|
const isDisabled =
|
|
157
161
|
typeof disabledOverride === 'boolean'
|
|
158
162
|
? disabledOverride
|
|
159
163
|
: !currentCompany || Object.keys(currentCompany).length === 0;
|
|
160
164
|
|
|
165
|
+
if (!navItems.length) return null;
|
|
166
|
+
|
|
161
167
|
return (
|
|
162
168
|
<BottomNavigationBar
|
|
163
169
|
appearance={appearance}
|
|
170
|
+
placement={placement}
|
|
164
171
|
activeRouteName={effectiveActiveRoute}
|
|
165
172
|
colors={colors}
|
|
166
173
|
disabled={isDisabled}
|
|
@@ -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) {
|
|
@@ -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
|
+
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
parseConfigsObject,
|
|
25
25
|
tt,
|
|
26
26
|
} from './deviceListHelpers';
|
|
27
|
-
import
|
|
27
|
+
import legacyStyles from '../../Devices.styles';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Normalize DefaultTable card `item` into a device group shape.
|
|
@@ -65,6 +65,7 @@ const normalizeDeviceGroup = item => {
|
|
|
65
65
|
export default function DeviceGroupCard({
|
|
66
66
|
item,
|
|
67
67
|
brandColors,
|
|
68
|
+
themedStyles = null,
|
|
68
69
|
creatingPdv,
|
|
69
70
|
goToDetail,
|
|
70
71
|
handleCreateCurrentPdv,
|
|
@@ -74,6 +75,7 @@ export default function DeviceGroupCard({
|
|
|
74
75
|
runtimeDeviceType,
|
|
75
76
|
showCurrentPdvSetup,
|
|
76
77
|
}) {
|
|
78
|
+
const styles = themedStyles || legacyStyles;
|
|
77
79
|
const deviceGroup = normalizeDeviceGroup(item);
|
|
78
80
|
if (!deviceGroup) {
|
|
79
81
|
return null;
|
|
@@ -116,7 +118,7 @@ export default function DeviceGroupCard({
|
|
|
116
118
|
deviceGroup.device?.id ||
|
|
117
119
|
deviceGroup.key,
|
|
118
120
|
);
|
|
119
|
-
const accent = getDeviceTypeAccent(normalizedType);
|
|
121
|
+
const accent = brandColors.deviceAccent || getDeviceTypeAccent(normalizedType);
|
|
120
122
|
const metaChips = [];
|
|
121
123
|
|
|
122
124
|
if (isPdv && primaryConfig) {
|
|
@@ -196,7 +198,7 @@ export default function DeviceGroupCard({
|
|
|
196
198
|
<View style={styles.deviceConfigRow}>
|
|
197
199
|
{deviceConfigs.map(deviceConfig => {
|
|
198
200
|
const configType = getDeviceConfigType(deviceConfig);
|
|
199
|
-
const configAccent = getDeviceTypeAccent(configType);
|
|
201
|
+
const configAccent = brandColors.deviceAccent || getDeviceTypeAccent(configType);
|
|
200
202
|
const isSessionConfig =
|
|
201
203
|
isCurrentDevice &&
|
|
202
204
|
runtimeDeviceType &&
|
|
@@ -297,5 +299,5 @@ export default function DeviceGroupCard({
|
|
|
297
299
|
</View>
|
|
298
300
|
</View>
|
|
299
301
|
);
|
|
300
|
-
|
|
302
|
+
|
|
301
303
|
}
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
prioritizeCurrentDeviceGroups,
|
|
37
37
|
readStoredRuntimeDevice,
|
|
38
38
|
} from '../currentDevice';
|
|
39
|
-
import
|
|
39
|
+
import legacyStyles, {createStyles} from '../../Devices.styles';
|
|
40
40
|
import DeviceGroupCard from './DeviceGroupCard';
|
|
41
41
|
import {useDeviceNetworkConnectivity} from './useDeviceNetworkConnectivity';
|
|
42
42
|
import {
|
|
@@ -89,7 +89,7 @@ export const createDeviceTypeTab = ({
|
|
|
89
89
|
clientFilter = null,
|
|
90
90
|
offerCurrentPdvSetup = false,
|
|
91
91
|
}) => {
|
|
92
|
-
const DeviceTypeTab = () => {
|
|
92
|
+
const DeviceTypeTab = ({themePalette = null} = {}) => {
|
|
93
93
|
const navigation = useNavigation();
|
|
94
94
|
const peopleStore = useStore('people');
|
|
95
95
|
const deviceStore = useStore('device');
|
|
@@ -101,13 +101,15 @@ export const createDeviceTypeTab = ({
|
|
|
101
101
|
|
|
102
102
|
const brandColors = useMemo(
|
|
103
103
|
() =>
|
|
104
|
-
resolveThemePalette(
|
|
104
|
+
themePalette || resolveThemePalette(
|
|
105
105
|
{...themeColors, ...(currentCompany?.theme?.colors || {})},
|
|
106
106
|
colors,
|
|
107
107
|
),
|
|
108
|
-
[themeColors, currentCompany?.id],
|
|
108
|
+
[themePalette, themeColors, currentCompany?.id],
|
|
109
109
|
);
|
|
110
110
|
|
|
111
|
+
const styles = useMemo(() => themePalette ? createStyles(brandColors) : legacyStyles, [themePalette, brandColors]);
|
|
112
|
+
|
|
111
113
|
const [deviceConfigs, setDeviceConfigs] = useState([]);
|
|
112
114
|
const [loading, setLoading] = useState(false);
|
|
113
115
|
const [refreshing, setRefreshing] = useState(false);
|
|
@@ -372,6 +374,7 @@ export const createDeviceTypeTab = ({
|
|
|
372
374
|
<DeviceGroupCard
|
|
373
375
|
{...cardProps}
|
|
374
376
|
brandColors={brandColors}
|
|
377
|
+
themedStyles={themePalette ? styles : null}
|
|
375
378
|
creatingPdv={creatingPdv}
|
|
376
379
|
goToDetail={goToDetail}
|
|
377
380
|
handleCreateCurrentPdv={handleCreateCurrentPdv}
|
|
@@ -384,6 +387,8 @@ export const createDeviceTypeTab = ({
|
|
|
384
387
|
),
|
|
385
388
|
[
|
|
386
389
|
brandColors,
|
|
390
|
+
styles,
|
|
391
|
+
themePalette,
|
|
387
392
|
creatingPdv,
|
|
388
393
|
goToDetail,
|
|
389
394
|
handleCreateCurrentPdv,
|
|
@@ -1,183 +1,184 @@
|
|
|
1
|
-
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
|
-
import {Text, TouchableOpacity, View} from 'react-native';
|
|
3
|
-
import {SafeAreaView} from 'react-native-safe-area-context';
|
|
4
|
-
import {useNavigation} from '@react-navigation/native';
|
|
5
|
-
import {useStore} from '@store';
|
|
6
|
-
import {resolveThemePalette} from '@controleonline/../../src/styles/branding';
|
|
7
|
-
import {colors} from '@controleonline/../../src/styles/colors';
|
|
8
|
-
import CompactFilterSelector from '@controleonline/ui-default/src/react/components/filters/CompactFilterSelector';
|
|
9
|
-
import {PDV_DEVICE_TYPE} from '@controleonline/ui-common/src/react/utils/printerDevices';
|
|
10
|
-
import Icon from 'react-native-vector-icons/Feather';
|
|
11
|
-
import {
|
|
12
|
-
ALL_DEVICE_FILTER,
|
|
13
|
-
getDeviceCreationAction,
|
|
14
|
-
getDeviceFilterAccent,
|
|
15
|
-
getDeviceFilterDefinition,
|
|
16
|
-
getDeviceFilterIcon,
|
|
17
|
-
getDeviceFilterOptions,
|
|
18
|
-
getPdvSubfilterOptions,
|
|
19
|
-
hasRegisteredDeviceFilter,
|
|
20
|
-
isPdvSubfilter,
|
|
21
|
-
} from './Devices/deviceTypes';
|
|
22
|
-
import { createStyles } from './Devices.styles';
|
|
23
|
-
|
|
24
|
-
const tt = (type, key) => global.t?.t('configs', type, key);
|
|
25
|
-
|
|
26
|
-
const Devices = () => {
|
|
27
|
-
const navigation = useNavigation();
|
|
28
|
-
const peopleStore = useStore('people');
|
|
29
|
-
const themeStore = useStore('theme');
|
|
30
|
-
|
|
31
|
-
const {currentCompany} = peopleStore.getters;
|
|
32
|
-
const {colors: themeColors} = themeStore.getters;
|
|
33
|
-
|
|
34
|
-
const [activeFilter, setActiveFilter] = useState(ALL_DEVICE_FILTER);
|
|
35
|
-
|
|
36
|
-
const brandColors = useMemo(
|
|
37
|
-
() =>
|
|
38
|
-
resolveThemePalette(
|
|
39
|
-
{...themeColors, ...(currentCompany?.theme?.colors || {})},
|
|
40
|
-
colors,
|
|
41
|
-
),
|
|
42
|
-
[themeColors, currentCompany?.id],
|
|
43
|
-
);
|
|
44
|
-
const styles = useMemo(() => createStyles(brandColors), [brandColors]);
|
|
45
|
-
const accentPalette = useMemo(
|
|
46
|
-
() => ({
|
|
47
|
-
primary: brandColors.info,
|
|
48
|
-
success: brandColors.success,
|
|
49
|
-
danger: brandColors.error,
|
|
50
|
-
warning: brandColors.warning,
|
|
51
|
-
}),
|
|
52
|
-
[brandColors.error, brandColors.info, brandColors.success, brandColors.warning],
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const filterOptions = useMemo(() => getDeviceFilterOptions(), []);
|
|
56
|
-
const pdvSubfilterOptions = useMemo(() => getPdvSubfilterOptions(), []);
|
|
57
|
-
const activeFilterDefinition = useMemo(
|
|
58
|
-
() => getDeviceFilterDefinition(activeFilter),
|
|
59
|
-
[activeFilter],
|
|
60
|
-
);
|
|
61
|
-
const topLevelActiveFilter = useMemo(
|
|
62
|
-
() => (isPdvSubfilter(activeFilter) ? PDV_DEVICE_TYPE : activeFilter),
|
|
63
|
-
[activeFilter],
|
|
64
|
-
);
|
|
65
|
-
const activeFilterAction = useMemo(
|
|
66
|
-
() => getDeviceCreationAction(topLevelActiveFilter),
|
|
67
|
-
[topLevelActiveFilter],
|
|
68
|
-
);
|
|
69
|
-
const deviceTypeSelectorOptions = useMemo(
|
|
70
|
-
() => filterOptions.map(option => ({
|
|
71
|
-
key: option.key,
|
|
72
|
-
label: option.label,
|
|
73
|
-
})),
|
|
74
|
-
[filterOptions],
|
|
75
|
-
);
|
|
76
|
-
const pdvGatewaySelectorOptions = useMemo(
|
|
77
|
-
() => [
|
|
78
|
-
{key: PDV_DEVICE_TYPE, label: tt('device_filter', 'all') || 'Todos'},
|
|
79
|
-
...pdvSubfilterOptions.map(option => ({
|
|
80
|
-
key: option.key,
|
|
81
|
-
label: option.label,
|
|
82
|
-
})),
|
|
83
|
-
],
|
|
84
|
-
[pdvSubfilterOptions],
|
|
85
|
-
);
|
|
86
|
-
const selectedDeviceTypeLabel = useMemo(
|
|
87
|
-
() =>
|
|
88
|
-
deviceTypeSelectorOptions.find(option => option.key === topLevelActiveFilter)?.label ||
|
|
89
|
-
tt('device_filter', 'allTypes') ||
|
|
90
|
-
'Todos os tipos',
|
|
91
|
-
[deviceTypeSelectorOptions, topLevelActiveFilter],
|
|
92
|
-
);
|
|
93
|
-
const selectedPdvGatewayLabel = useMemo(
|
|
94
|
-
() =>
|
|
95
|
-
pdvGatewaySelectorOptions.find(option => option.key === activeFilter)?.label ||
|
|
96
|
-
tt('device_filter', 'all') ||
|
|
97
|
-
'Todos',
|
|
98
|
-
[activeFilter, pdvGatewaySelectorOptions],
|
|
99
|
-
);
|
|
100
|
-
const ActiveTabComponent = activeFilterDefinition.TabComponent;
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
if (!hasRegisteredDeviceFilter(activeFilter)) {
|
|
104
|
-
setActiveFilter(ALL_DEVICE_FILTER);
|
|
105
|
-
}
|
|
106
|
-
}, [activeFilter]);
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<SafeAreaView
|
|
110
|
-
style={[styles.container, {backgroundColor: brandColors.background}]}>
|
|
111
|
-
<View style={styles.filtersBlock}>
|
|
112
|
-
<Text style={styles.filtersLabel}>{tt('device_filter', 'filterByType') || 'Filtrar por tipo'}</Text>
|
|
113
|
-
<CompactFilterSelector
|
|
114
|
-
icon={getDeviceFilterIcon(topLevelActiveFilter)}
|
|
115
|
-
label={selectedDeviceTypeLabel}
|
|
116
|
-
title={tt('device_filter', 'filterByType') || 'Filtrar por tipo'}
|
|
117
|
-
accentColor={getDeviceFilterAccent(topLevelActiveFilter, {
|
|
118
|
-
brandColors,
|
|
119
|
-
hex: accentPalette,
|
|
120
|
-
})}
|
|
121
|
-
active={topLevelActiveFilter !== ALL_DEVICE_FILTER}
|
|
122
|
-
options={deviceTypeSelectorOptions}
|
|
123
|
-
selectedKey={topLevelActiveFilter}
|
|
124
|
-
onSelect={optionKey => {
|
|
125
|
-
setActiveFilter(optionKey);
|
|
126
|
-
return true;
|
|
127
|
-
}}
|
|
128
|
-
/>
|
|
129
|
-
</View>
|
|
130
|
-
|
|
131
|
-
{topLevelActiveFilter === PDV_DEVICE_TYPE && pdvSubfilterOptions.length > 0 ? (
|
|
132
|
-
<View style={styles.filtersBlock}>
|
|
133
|
-
<Text style={styles.filtersLabel}>{tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}</Text>
|
|
134
|
-
<CompactFilterSelector
|
|
135
|
-
icon={getDeviceFilterIcon(activeFilter)}
|
|
136
|
-
label={selectedPdvGatewayLabel}
|
|
137
|
-
title={tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}
|
|
138
|
-
accentColor={getDeviceFilterAccent(activeFilter, {
|
|
139
|
-
brandColors,
|
|
140
|
-
hex: accentPalette,
|
|
141
|
-
})}
|
|
142
|
-
active={activeFilter !== PDV_DEVICE_TYPE}
|
|
143
|
-
options={pdvGatewaySelectorOptions}
|
|
144
|
-
selectedKey={activeFilter}
|
|
145
|
-
onSelect={optionKey => {
|
|
146
|
-
setActiveFilter(optionKey);
|
|
147
|
-
return true;
|
|
148
|
-
}}
|
|
149
|
-
/>
|
|
150
|
-
</View>
|
|
151
|
-
) : null}
|
|
152
|
-
|
|
153
|
-
{activeFilterAction ? (
|
|
154
|
-
<View style={styles.printerActionBlock}>
|
|
155
|
-
<TouchableOpacity
|
|
156
|
-
style={[
|
|
157
|
-
styles.createPrinterBtn,
|
|
158
|
-
{backgroundColor: brandColors.primary},
|
|
159
|
-
]}
|
|
160
|
-
activeOpacity={0.86}
|
|
161
|
-
onPress={() => navigation.navigate(activeFilterAction.routeName)}>
|
|
162
|
-
<Icon name="plus-circle" size={16} color={brandColors.white} />
|
|
163
|
-
<Text style={styles.createPrinterBtnText}>
|
|
164
|
-
{activeFilterAction.label}
|
|
165
|
-
</Text>
|
|
166
|
-
</TouchableOpacity>
|
|
167
|
-
|
|
168
|
-
<Text style={styles.helperText}>
|
|
169
|
-
{activeFilterAction.helperText}
|
|
170
|
-
</Text>
|
|
171
|
-
</View>
|
|
172
|
-
) : null}
|
|
173
|
-
|
|
174
|
-
{ActiveTabComponent ? (
|
|
175
|
-
<ActiveTabComponent
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
|
+
import {Text, TouchableOpacity, View} from 'react-native';
|
|
3
|
+
import {SafeAreaView} from 'react-native-safe-area-context';
|
|
4
|
+
import {useNavigation} from '@react-navigation/native';
|
|
5
|
+
import {useStore} from '@store';
|
|
6
|
+
import {resolveThemePalette} from '@controleonline/../../src/styles/branding';
|
|
7
|
+
import {colors} from '@controleonline/../../src/styles/colors';
|
|
8
|
+
import CompactFilterSelector from '@controleonline/ui-default/src/react/components/filters/CompactFilterSelector';
|
|
9
|
+
import {PDV_DEVICE_TYPE} from '@controleonline/ui-common/src/react/utils/printerDevices';
|
|
10
|
+
import Icon from 'react-native-vector-icons/Feather';
|
|
11
|
+
import {
|
|
12
|
+
ALL_DEVICE_FILTER,
|
|
13
|
+
getDeviceCreationAction,
|
|
14
|
+
getDeviceFilterAccent,
|
|
15
|
+
getDeviceFilterDefinition,
|
|
16
|
+
getDeviceFilterIcon,
|
|
17
|
+
getDeviceFilterOptions,
|
|
18
|
+
getPdvSubfilterOptions,
|
|
19
|
+
hasRegisteredDeviceFilter,
|
|
20
|
+
isPdvSubfilter,
|
|
21
|
+
} from './Devices/deviceTypes';
|
|
22
|
+
import { createStyles } from './Devices.styles';
|
|
23
|
+
|
|
24
|
+
const tt = (type, key) => global.t?.t('configs', type, key);
|
|
25
|
+
|
|
26
|
+
const Devices = ({themePalette = null} = {}) => {
|
|
27
|
+
const navigation = useNavigation();
|
|
28
|
+
const peopleStore = useStore('people');
|
|
29
|
+
const themeStore = useStore('theme');
|
|
30
|
+
|
|
31
|
+
const {currentCompany} = peopleStore.getters;
|
|
32
|
+
const {colors: themeColors} = themeStore.getters;
|
|
33
|
+
|
|
34
|
+
const [activeFilter, setActiveFilter] = useState(ALL_DEVICE_FILTER);
|
|
35
|
+
|
|
36
|
+
const brandColors = useMemo(
|
|
37
|
+
() =>
|
|
38
|
+
themePalette || resolveThemePalette(
|
|
39
|
+
{...themeColors, ...(currentCompany?.theme?.colors || {})},
|
|
40
|
+
colors,
|
|
41
|
+
),
|
|
42
|
+
[themePalette, themeColors, currentCompany?.id],
|
|
43
|
+
);
|
|
44
|
+
const styles = useMemo(() => createStyles(brandColors), [brandColors]);
|
|
45
|
+
const accentPalette = useMemo(
|
|
46
|
+
() => ({
|
|
47
|
+
primary: brandColors.info,
|
|
48
|
+
success: brandColors.success,
|
|
49
|
+
danger: brandColors.error,
|
|
50
|
+
warning: brandColors.warning,
|
|
51
|
+
}),
|
|
52
|
+
[brandColors.error, brandColors.info, brandColors.success, brandColors.warning],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const filterOptions = useMemo(() => getDeviceFilterOptions(), []);
|
|
56
|
+
const pdvSubfilterOptions = useMemo(() => getPdvSubfilterOptions(), []);
|
|
57
|
+
const activeFilterDefinition = useMemo(
|
|
58
|
+
() => getDeviceFilterDefinition(activeFilter),
|
|
59
|
+
[activeFilter],
|
|
60
|
+
);
|
|
61
|
+
const topLevelActiveFilter = useMemo(
|
|
62
|
+
() => (isPdvSubfilter(activeFilter) ? PDV_DEVICE_TYPE : activeFilter),
|
|
63
|
+
[activeFilter],
|
|
64
|
+
);
|
|
65
|
+
const activeFilterAction = useMemo(
|
|
66
|
+
() => getDeviceCreationAction(topLevelActiveFilter),
|
|
67
|
+
[topLevelActiveFilter],
|
|
68
|
+
);
|
|
69
|
+
const deviceTypeSelectorOptions = useMemo(
|
|
70
|
+
() => filterOptions.map(option => ({
|
|
71
|
+
key: option.key,
|
|
72
|
+
label: option.label,
|
|
73
|
+
})),
|
|
74
|
+
[filterOptions],
|
|
75
|
+
);
|
|
76
|
+
const pdvGatewaySelectorOptions = useMemo(
|
|
77
|
+
() => [
|
|
78
|
+
{key: PDV_DEVICE_TYPE, label: tt('device_filter', 'all') || 'Todos'},
|
|
79
|
+
...pdvSubfilterOptions.map(option => ({
|
|
80
|
+
key: option.key,
|
|
81
|
+
label: option.label,
|
|
82
|
+
})),
|
|
83
|
+
],
|
|
84
|
+
[pdvSubfilterOptions],
|
|
85
|
+
);
|
|
86
|
+
const selectedDeviceTypeLabel = useMemo(
|
|
87
|
+
() =>
|
|
88
|
+
deviceTypeSelectorOptions.find(option => option.key === topLevelActiveFilter)?.label ||
|
|
89
|
+
tt('device_filter', 'allTypes') ||
|
|
90
|
+
'Todos os tipos',
|
|
91
|
+
[deviceTypeSelectorOptions, topLevelActiveFilter],
|
|
92
|
+
);
|
|
93
|
+
const selectedPdvGatewayLabel = useMemo(
|
|
94
|
+
() =>
|
|
95
|
+
pdvGatewaySelectorOptions.find(option => option.key === activeFilter)?.label ||
|
|
96
|
+
tt('device_filter', 'all') ||
|
|
97
|
+
'Todos',
|
|
98
|
+
[activeFilter, pdvGatewaySelectorOptions],
|
|
99
|
+
);
|
|
100
|
+
const ActiveTabComponent = activeFilterDefinition.TabComponent;
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!hasRegisteredDeviceFilter(activeFilter)) {
|
|
104
|
+
setActiveFilter(ALL_DEVICE_FILTER);
|
|
105
|
+
}
|
|
106
|
+
}, [activeFilter]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<SafeAreaView
|
|
110
|
+
style={[styles.container, {backgroundColor: brandColors.background}]}>
|
|
111
|
+
<View style={styles.filtersBlock}>
|
|
112
|
+
<Text style={styles.filtersLabel}>{tt('device_filter', 'filterByType') || 'Filtrar por tipo'}</Text>
|
|
113
|
+
<CompactFilterSelector
|
|
114
|
+
icon={getDeviceFilterIcon(topLevelActiveFilter)}
|
|
115
|
+
label={selectedDeviceTypeLabel}
|
|
116
|
+
title={tt('device_filter', 'filterByType') || 'Filtrar por tipo'}
|
|
117
|
+
accentColor={getDeviceFilterAccent(topLevelActiveFilter, {
|
|
118
|
+
brandColors,
|
|
119
|
+
hex: accentPalette,
|
|
120
|
+
})}
|
|
121
|
+
active={topLevelActiveFilter !== ALL_DEVICE_FILTER}
|
|
122
|
+
options={deviceTypeSelectorOptions}
|
|
123
|
+
selectedKey={topLevelActiveFilter}
|
|
124
|
+
onSelect={optionKey => {
|
|
125
|
+
setActiveFilter(optionKey);
|
|
126
|
+
return true;
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
</View>
|
|
130
|
+
|
|
131
|
+
{topLevelActiveFilter === PDV_DEVICE_TYPE && pdvSubfilterOptions.length > 0 ? (
|
|
132
|
+
<View style={styles.filtersBlock}>
|
|
133
|
+
<Text style={styles.filtersLabel}>{tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}</Text>
|
|
134
|
+
<CompactFilterSelector
|
|
135
|
+
icon={getDeviceFilterIcon(activeFilter)}
|
|
136
|
+
label={selectedPdvGatewayLabel}
|
|
137
|
+
title={tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}
|
|
138
|
+
accentColor={getDeviceFilterAccent(activeFilter, {
|
|
139
|
+
brandColors,
|
|
140
|
+
hex: accentPalette,
|
|
141
|
+
})}
|
|
142
|
+
active={activeFilter !== PDV_DEVICE_TYPE}
|
|
143
|
+
options={pdvGatewaySelectorOptions}
|
|
144
|
+
selectedKey={activeFilter}
|
|
145
|
+
onSelect={optionKey => {
|
|
146
|
+
setActiveFilter(optionKey);
|
|
147
|
+
return true;
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
</View>
|
|
151
|
+
) : null}
|
|
152
|
+
|
|
153
|
+
{activeFilterAction ? (
|
|
154
|
+
<View style={styles.printerActionBlock}>
|
|
155
|
+
<TouchableOpacity
|
|
156
|
+
style={[
|
|
157
|
+
styles.createPrinterBtn,
|
|
158
|
+
{backgroundColor: brandColors.primary},
|
|
159
|
+
]}
|
|
160
|
+
activeOpacity={0.86}
|
|
161
|
+
onPress={() => navigation.navigate(activeFilterAction.routeName)}>
|
|
162
|
+
<Icon name="plus-circle" size={16} color={brandColors.white} />
|
|
163
|
+
<Text style={styles.createPrinterBtnText}>
|
|
164
|
+
{activeFilterAction.label}
|
|
165
|
+
</Text>
|
|
166
|
+
</TouchableOpacity>
|
|
167
|
+
|
|
168
|
+
<Text style={styles.helperText}>
|
|
169
|
+
{activeFilterAction.helperText}
|
|
170
|
+
</Text>
|
|
171
|
+
</View>
|
|
172
|
+
) : null}
|
|
173
|
+
|
|
174
|
+
{ActiveTabComponent ? (
|
|
175
|
+
<ActiveTabComponent
|
|
176
|
+
themePalette={themePalette}
|
|
177
|
+
key={`${activeFilter}-${currentCompany?.id || 'company'}`}
|
|
178
|
+
/>
|
|
179
|
+
) : null}
|
|
180
|
+
</SafeAreaView>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export default Devices;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import eventBus from '../components/EventBus';
|
|
2
|
+
|
|
3
|
+
const key = 'product-catalog-cache-revision';
|
|
4
|
+
let revision = '0';
|
|
5
|
+
let sequence = 0;
|
|
6
|
+
export const getProductCatalogCacheRevision = () => {
|
|
7
|
+
try { return localStorage.getItem(key) || revision; } catch { return revision; }
|
|
8
|
+
};
|
|
9
|
+
export const invalidateProductCatalogCache = () => {
|
|
10
|
+
revision = `${Date.now()}:${++sequence}`;
|
|
11
|
+
try { localStorage.setItem(key, revision); } catch { /* In-memory invalidation still applies. */ }
|
|
12
|
+
eventBus.emit('product-catalog-cache:cleared');
|
|
13
|
+
};
|
|
@@ -128,6 +128,26 @@ const formatRuntimeFooterVersion = version => {
|
|
|
128
128
|
return normalizedVersion ? `v${normalizedVersion}` : '';
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
+
const IPV4_PATTERN = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
132
|
+
|
|
133
|
+
// IPv4 is shown in full; other ':'-separated ids (IPv6/MAC-like) are shortened
|
|
134
|
+
// to an ellipsis plus the last two blocks, e.g. "…46f4:eb0b".
|
|
135
|
+
const formatRuntimeFooterIdentifier = value => {
|
|
136
|
+
const identifier = safeTrim(value);
|
|
137
|
+
|
|
138
|
+
if (!identifier || IPV4_PATTERN.test(identifier) || !identifier.includes(':')) {
|
|
139
|
+
return identifier;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const blocks = identifier.split(':').filter(Boolean);
|
|
143
|
+
|
|
144
|
+
if (blocks.length <= 2) {
|
|
145
|
+
return identifier;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return `…${blocks.slice(-2).join(':')}`;
|
|
149
|
+
};
|
|
150
|
+
|
|
131
151
|
const getRuntimeFooterWebHost = () => {
|
|
132
152
|
const hostname = safeTrim(globalThis?.location?.hostname);
|
|
133
153
|
|
|
@@ -364,9 +384,9 @@ const getRuntimeFooterDebugInfo = ({device, appVersion, deviceConfig}) => {
|
|
|
364
384
|
const nativeIdentifier = nativeIdentifierCandidates[0]?.value || '';
|
|
365
385
|
const runtimeDetail = operationModeLabel
|
|
366
386
|
? operationModeLabel
|
|
367
|
-
:
|
|
368
|
-
? webIdentifier
|
|
369
|
-
|
|
387
|
+
: formatRuntimeFooterIdentifier(
|
|
388
|
+
isWebRuntime ? webIdentifier : nativeIdentifier,
|
|
389
|
+
);
|
|
370
390
|
const runtimeDetailSource = operationModeLabel
|
|
371
391
|
? 'device_config.configs.pos-operation-mode'
|
|
372
392
|
: isWebRuntime
|
|
@@ -444,6 +464,7 @@ module.exports = {
|
|
|
444
464
|
getRuntimeFooterStoredVersion,
|
|
445
465
|
getRuntimeFooterTextLines,
|
|
446
466
|
getRuntimeFooterVersionCandidates,
|
|
467
|
+
formatRuntimeFooterIdentifier,
|
|
447
468
|
getRuntimeFooterWebIdentifierCandidates,
|
|
448
469
|
getRuntimeFooterText,
|
|
449
470
|
getRuntimeFooterWebHost,
|
|
@@ -2,6 +2,7 @@ const assert = require('node:assert/strict')
|
|
|
2
2
|
const {after, before, test} = require('node:test')
|
|
3
3
|
|
|
4
4
|
const {
|
|
5
|
+
formatRuntimeFooterIdentifier,
|
|
5
6
|
getRuntimeFooterDebugInfo,
|
|
6
7
|
getRuntimeFooterNativeIdentifierCandidates,
|
|
7
8
|
getRuntimeFooterPrimaryText,
|
|
@@ -487,3 +488,28 @@ test('hides runtime device information on the customer paylist only', () => {
|
|
|
487
488
|
assert.equal(shouldShowRuntimeFooter('LoginPage'), true)
|
|
488
489
|
assert.equal(shouldShowRuntimeFooter('WalletsPage'), true)
|
|
489
490
|
})
|
|
491
|
+
|
|
492
|
+
test('formats runtime identifier: IPv4 full, non-IPv4 shortened to last two blocks', () => {
|
|
493
|
+
assert.equal(formatRuntimeFooterIdentifier('179.110.95.138'), '179.110.95.138')
|
|
494
|
+
assert.equal(
|
|
495
|
+
formatRuntimeFooterIdentifier('2804:18:1119:f433:54b4:a587:46f4:eb0b'),
|
|
496
|
+
'…46f4:eb0b',
|
|
497
|
+
)
|
|
498
|
+
assert.equal(formatRuntimeFooterIdentifier('aa:bb:cc:dd:ee:ff'), '…ee:ff')
|
|
499
|
+
assert.equal(formatRuntimeFooterIdentifier('::1'), '::1')
|
|
500
|
+
assert.equal(formatRuntimeFooterIdentifier('abc'), 'abc')
|
|
501
|
+
assert.equal(formatRuntimeFooterIdentifier(''), '')
|
|
502
|
+
assert.equal(formatRuntimeFooterIdentifier(null), '')
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
test('primary text shortens IPv6 public ip', () => {
|
|
506
|
+
const text = getRuntimeFooterPrimaryText({
|
|
507
|
+
device: {
|
|
508
|
+
type: 'PDV',
|
|
509
|
+
systemName: 'web',
|
|
510
|
+
metadata: {network: {publicIp: '2804:18:1119:f433:54b4:a587:46f4:eb0b'}},
|
|
511
|
+
},
|
|
512
|
+
appVersion: '1.10.68',
|
|
513
|
+
})
|
|
514
|
+
assert.ok(text.includes('(…46f4:eb0b)'), text)
|
|
515
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const {test} = require('node:test');
|
|
3
|
+
const {readStoredSessionToken} = require('../../../utils/storedSessionToken');
|
|
4
|
+
|
|
5
|
+
for (const prefix of ['', '__q_objt|']) {
|
|
6
|
+
for (const field of ['token', 'api_key']) {
|
|
7
|
+
test(`reads ${field} from ${prefix ? 'wrapped' : 'plain'} session`, () => {
|
|
8
|
+
const storage = {getItem: key => key === 'session' ? prefix + JSON.stringify({[field]: 'synthetic-token'}) : null};
|
|
9
|
+
assert.equal(readStoredSessionToken(storage), 'synthetic-token');
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
test('prefers the canonical token field when both are present', () => {
|
|
14
|
+
assert.equal(readStoredSessionToken({getItem: () => JSON.stringify({token: 'current', api_key: 'old'})}), 'current');
|
|
15
|
+
});
|
|
16
|
+
test('missing, malformed and inaccessible sessions have no token', () => {
|
|
17
|
+
for (const raw of [null, '', 'broken', '__q_objt|broken', '{}', 'null']) {
|
|
18
|
+
assert.equal(readStoredSessionToken({getItem: () => raw}), '');
|
|
19
|
+
}
|
|
20
|
+
assert.equal(readStoredSessionToken({getItem: () => {throw new Error('unavailable');}}), '');
|
|
21
|
+
assert.equal(readStoredSessionToken(null), '');
|
|
22
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Quasar LocalStorage serializes objects with this marker; plain JSON is also used.
|
|
2
|
+
const OBJECT_PREFIX = '__q_objt|';
|
|
3
|
+
|
|
4
|
+
const readStoredSessionToken = (storage = typeof localStorage === 'undefined' ? null : localStorage) => {
|
|
5
|
+
try {
|
|
6
|
+
const raw = storage?.getItem?.('session');
|
|
7
|
+
if (!raw) return '';
|
|
8
|
+
const json = typeof raw === 'string' && raw.startsWith(OBJECT_PREFIX)
|
|
9
|
+
? raw.slice(OBJECT_PREFIX.length)
|
|
10
|
+
: raw;
|
|
11
|
+
const session = JSON.parse(json);
|
|
12
|
+
return session?.token || session?.api_key || '';
|
|
13
|
+
} catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = {readStoredSessionToken};
|
package/docs/homepage-1056.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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.96.
|
|
13
|
-
|
|
14
|
-
Release 1.2.96 uses the task-only c91510d source plus version metadata. The integration merge into dev preserves independent team work, but those changes are excluded from this homepage package. Publication uses the existing tag-triggered trusted publishing workflow.
|
|
15
|
-
|
|
16
|
-
Release 1.2.97 completes the web bootstrap gate with visibility:hidden and aria-hidden: pending descendants cannot receive keyboard focus while navigation stays mounted. Native behavior remains unchanged.
|