@controleonline/ui-common 1.2.94 → 1.2.95

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.
@@ -4,3 +4,9 @@ Soft appearance is opt-in for HomePage only. Existing menu routes/parameters, pe
4
4
 
5
5
  Source: https://github.com/ControleOnline/app-community/issues/1056
6
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@controleonline/ui-common",
3
- "version": "1.2.94",
3
+ "version": "1.2.95",
4
4
  "author": "ControleOnline",
5
5
  "description": "ControleOnline Common Quasar UI Component",
6
6
  "license": "MIT",
@@ -12,6 +12,9 @@ const styles = StyleSheet.create({
12
12
  shell: {
13
13
  flex: 1,
14
14
  },
15
+ translationPending: {
16
+ opacity: 0,
17
+ },
15
18
  content: {
16
19
  flex: 1,
17
20
  minHeight: 0,
@@ -395,15 +395,19 @@ export const DefaultProvider = ({
395
395
 
396
396
  useWebThemePalette({resolveDomainThemeColors, baseThemeColors, mainCompany, resolveThemePalette, runtimeColors, applyPaletteToRuntimeColors, applyThemeCssVariables, actions, currentCompany, isShopClientApp});
397
397
 
398
- if (requiresTranslateBootstrap && !isTranslateBootstrapReady) {
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
+ accessibilityElementsHidden={translationPending}
405
+ importantForAccessibility={translationPending ? 'no-hide-descendants' : 'auto'}
405
406
  style={[
406
407
  providerStyles.shell,
408
+ // Keep navigation mounted while translations load. Unmounting it
409
+ // resets the login route and repeatedly registers the bottom bar.
410
+ translationPending && providerStyles.translationPending,
407
411
  {
408
412
  backgroundColor: colors?.background || runtimeColors.background,
409
413
  },
@@ -1,4 +1,6 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ lines 378
2
+ ok True
3
+ eCallback, useEffect, useMemo, useRef, useState } from 'react';
2
4
  import { ActivityIndicator, Platform, RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
3
5
  import { SafeAreaView } from 'react-native-safe-area-context';
4
6
  import { useFocusEffect } from '@react-navigation/native';
@@ -14,6 +16,7 @@ import { fetchPeopleConfigs } from './fetchPeopleConfigs';
14
16
  import IntegrationConfigFields from './IntegrationConfigFields';
15
17
  import {
16
18
  ROUTE_PROVIDER_MAP,
19
+ expandBundledConfig,
17
20
  extractAuthorizationUrl,
18
21
  formatApiError,
19
22
  formatUberOAuthError,
@@ -112,6 +115,8 @@ export default function IntegrationConfigPage({ route, embedded = false, fiscalT
112
115
  const allFieldKeysSig = useMemo(() => allFieldKeys.join('|'), [allFieldKeys]);
113
116
  // Pure config providers (e.g. receita-federal) do not use marketplace/integrations.
114
117
  const needsMarketplaceIntegration = Boolean(providerConfig?.oauthConnect);
118
+ const bundleKey = normalizeTextValue(providerConfig?.bundleKey);
119
+ const configVisibility = normalizeTextValue(providerConfig?.visibility) || 'public';
115
120
 
116
121
  useEffect(() => {
117
122
  setConfigValues({});
@@ -135,14 +140,28 @@ export default function IntegrationConfigPage({ route, embedded = false, fiscalT
135
140
  }
136
141
  if (showLoading) setLoading(true);
137
142
  try {
138
- const keys = allFieldKeys.length
139
- ? allFieldKeys
140
- : visibleFields.map(field => field.key).filter(Boolean);
141
-
142
- if (keys.length) {
143
- const configItems = await fetchPeopleConfigs({ peopleIri: providerIri, configKeys: keys });
143
+ if (bundleKey) {
144
+ const configItems = await fetchPeopleConfigs({
145
+ peopleIri: providerIri,
146
+ configKeys: [bundleKey],
147
+ });
148
+ const bundled = configItems.find(
149
+ item => String(item?.configKey || '').trim() === bundleKey,
150
+ );
144
151
  const mergeFields = allConfigFields.length ? allConfigFields : visibleFields;
145
- setConfigValues(current => mergeTabValues(mergeFields, configItems, current));
152
+ setConfigValues(current =>
153
+ expandBundledConfig(mergeFields, bundled?.configValue, current),
154
+ );
155
+ } else {
156
+ const keys = allFieldKeys.length
157
+ ? allFieldKeys
158
+ : visibleFields.map(field => field.key).filter(Boolean);
159
+
160
+ if (keys.length) {
161
+ const configItems = await fetchPeopleConfigs({ peopleIri: providerIri, configKeys: keys });
162
+ const mergeFields = allConfigFields.length ? allConfigFields : visibleFields;
163
+ setConfigValues(current => mergeTabValues(mergeFields, configItems, current));
164
+ }
146
165
  }
147
166
 
148
167
  if (needsMarketplaceIntegration) {
@@ -170,6 +189,7 @@ export default function IntegrationConfigPage({ route, embedded = false, fiscalT
170
189
  allConfigFields,
171
190
  allFieldKeys,
172
191
  allFieldKeysSig,
192
+ bundleKey,
173
193
  needsMarketplaceIntegration,
174
194
  providerConfig,
175
195
  providerIri,
@@ -246,16 +266,44 @@ export default function IntegrationConfigPage({ route, embedded = false, fiscalT
246
266
  showError('Nao foi possivel identificar a integracao selecionada.');
247
267
  return;
248
268
  }
249
- const configs = visibleFields.map(field => ({
250
- configKey: field.key,
251
- configValue: toConfigRequestValue(normalizeTextValue(configValues[field.key])),
252
- }));
269
+
270
+ let configs;
271
+ if (bundleKey) {
272
+ const payloadObject = {};
273
+ (visibleFields || []).forEach(field => {
274
+ payloadObject[field.key] = normalizeTextValue(configValues[field.key]);
275
+ });
276
+ configs = [
277
+ {
278
+ configKey: bundleKey,
279
+ configValue: toConfigRequestValue(payloadObject),
280
+ },
281
+ ];
282
+ } else {
283
+ configs = visibleFields.map(field => ({
284
+ configKey: field.key,
285
+ configValue: toConfigRequestValue(normalizeTextValue(configValues[field.key])),
286
+ }));
287
+ }
288
+
253
289
  try {
254
- await persistConfigs({ configs, people: providerIri, module: 4, visibility: 'public' });
290
+ await persistConfigs({ configs, people: providerIri, module: 4, visibility: configVisibility });
255
291
  showSuccess(`${activeTabDef?.label || providerConfig.label} salvo com sucesso.`);
256
292
  await loadPageData({ showLoading: false });
257
293
  } catch (error) { showError(error?.message || 'Nao foi possivel salvar a integracao.'); }
258
- }, [activeTabDef, configValues, loadPageData, persistConfigs, providerConfig, providerIri, showError, showSuccess, visibleFields]);
294
+ }, [
295
+ activeTabDef,
296
+ bundleKey,
297
+ configValues,
298
+ configVisibility,
299
+ loadPageData,
300
+ persistConfigs,
301
+ providerConfig,
302
+ providerIri,
303
+ showError,
304
+ showSuccess,
305
+ visibleFields,
306
+ ]);
259
307
 
260
308
  if (!providerConfig) return <CenterState icon="alert-triangle" iconColor="#e67e22" title="Integracao indisponivel" text="A tela solicitada nao possui configuracao cadastrada." />;
261
309
  if (!providerId) return <CenterState icon="building" iconColor="#94A3B8" title="Empresa nao identificada" text="Abra a ficha da empresa para gravar as configuracoes fiscais nela." />;
@@ -5,6 +5,7 @@ export const ROUTE_PROVIDER_MAP = {
5
5
  AsaasIntegrationPage: 'asaas',
6
6
  ClickSignIntegrationPage: 'clicksign',
7
7
  ReceitaFederalIntegrationPage: 'receita-federal',
8
+ SemPararIntegrationPage: 'sem-parar',
8
9
  };
9
10
 
10
11
  export const routeNameToPath = routeName =>
@@ -53,6 +54,22 @@ export const mergeTabValues = (fields, source, current = {}) => {
53
54
  return next;
54
55
  };
55
56
 
57
+ /** Expand a single JSON config (bundleKey) into flat field values. */
58
+ export const expandBundledConfig = (fields, rawValue, current = {}) => {
59
+ let parsed = rawValue;
60
+ if (typeof rawValue === 'string') {
61
+ try {
62
+ parsed = JSON.parse(rawValue);
63
+ } catch {
64
+ parsed = {};
65
+ }
66
+ }
67
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
68
+ parsed = {};
69
+ }
70
+ return mergeTabValues(fields, parsed, current);
71
+ };
72
+
56
73
  export const buildFieldValues = (providerConfig, source) =>
57
74
  mergeTabValues(getConfigFields(providerConfig), source, {});
58
75
 
@@ -70,6 +70,16 @@ export const INTEGRATION_LIST = [
70
70
  accent: '#166534',
71
71
  icon: 'file-text',
72
72
  },
73
+ {
74
+ key: 'sem-parar',
75
+ label: 'Sem Parar',
76
+ route: 'IntegrationConfigPage',
77
+ routeParams: {
78
+ providerKey: 'sem-parar',
79
+ },
80
+ accent: '#0EA5E9',
81
+ icon: 'truck',
82
+ },
73
83
  ];
74
84
 
75
85
  export const INTEGRATION_CONFIGS = {
@@ -257,6 +267,67 @@ export const INTEGRATION_CONFIGS = {
257
267
  ],
258
268
  fields: [],
259
269
  },
270
+
271
+ 'sem-parar': {
272
+ key: 'sem-parar',
273
+ label: 'Sem Parar',
274
+ accent: '#0EA5E9',
275
+ icon: 'truck',
276
+ description:
277
+ 'Credenciais da empresa para emissao de vale-pedagio via Sem Parar.',
278
+ saveLabel: 'Salvar Sem Parar',
279
+ // Backend (api-platform-integration SemPararConfig) reads a single private JSON key.
280
+ bundleKey: 'SEM_PARAR',
281
+ visibility: 'private',
282
+ requiredKeys: ['ENABLED', 'CLIENT_ID', 'CLIENT_SECRET', 'USERNAME', 'PASSWORD'],
283
+ fields: [
284
+ {
285
+ key: 'ENABLED',
286
+ label: 'Ativo',
287
+ type: 'select',
288
+ options: [
289
+ {value: '1', label: 'Sim'},
290
+ {value: '0', label: 'Nao'},
291
+ ],
292
+ },
293
+ {
294
+ key: 'ENVIRONMENT',
295
+ label: 'Ambiente',
296
+ type: 'select',
297
+ options: [
298
+ {value: 'sandbox', label: 'Sandbox'},
299
+ {value: 'production', label: 'Producao'},
300
+ ],
301
+ },
302
+ {
303
+ key: 'CLIENT_ID',
304
+ label: 'Client ID',
305
+ placeholder: 'Client ID Sem Parar',
306
+ },
307
+ {
308
+ key: 'CLIENT_SECRET',
309
+ label: 'Client secret',
310
+ placeholder: 'Client secret Sem Parar',
311
+ secureTextEntry: true,
312
+ },
313
+ {
314
+ key: 'USERNAME',
315
+ label: 'Usuario',
316
+ placeholder: 'Usuario da API',
317
+ },
318
+ {
319
+ key: 'PASSWORD',
320
+ label: 'Senha',
321
+ placeholder: 'Senha da API',
322
+ secureTextEntry: true,
323
+ },
324
+ {
325
+ key: 'CONVENIADO',
326
+ label: 'Conveniado',
327
+ placeholder: 'Codigo do conveniado (se aplicavel)',
328
+ },
329
+ ],
330
+ },
260
331
  };
261
332
 
262
333
  export const getIntegrationListItem = key =>
@@ -1,6 +1,7 @@
1
1
  export const CIELO_CONFIG_KEY = 'CIELO';
2
2
  export const NEW_RELIC_CONFIG_KEY = 'NEW_RELIC';
3
3
  export const SPOTIFY_CONFIG_KEY = 'SPOTIFY';
4
+ export const SEM_PARAR_CONFIG_KEY = 'SEM_PARAR';
4
5
 
5
6
  export const DEFAULT_CIELO_CONFIG = {
6
7
  ACCESS_TOKEN: '',
@@ -23,6 +24,21 @@ export const DEFAULT_SPOTIFY_CONFIG = {
23
24
  CLIENT_SECRET: '',
24
25
  };
25
26
 
27
+ /**
28
+ * Company-scoped Sem Parar credentials for vale-pedágio emission.
29
+ * ENABLED: "1" | "0"
30
+ * ENVIRONMENT: "sandbox" | "production"
31
+ */
32
+ export const DEFAULT_SEM_PARAR_CONFIG = {
33
+ ENABLED: '0',
34
+ CLIENT_ID: '',
35
+ CLIENT_SECRET: '',
36
+ USERNAME: '',
37
+ PASSWORD: '',
38
+ ENVIRONMENT: 'sandbox',
39
+ CONVENIADO: '',
40
+ };
41
+
26
42
  const isConfigMap = value =>
27
43
  value && typeof value === 'object' && !Array.isArray(value);
28
44
 
@@ -78,3 +94,10 @@ export const resolveSpotifyConfig = configs =>
78
94
  SPOTIFY_CONFIG_KEY,
79
95
  DEFAULT_SPOTIFY_CONFIG,
80
96
  );
97
+
98
+ export const resolveSemPararConfig = configs =>
99
+ resolveIntegrationConfig(
100
+ configs,
101
+ SEM_PARAR_CONFIG_KEY,
102
+ DEFAULT_SEM_PARAR_CONFIG,
103
+ );