@archbase/components 4.0.41 → 4.0.43
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/dist/archbase-components-4.0.43.tgz +0 -0
- package/dist/editors/ArchbasePasswordEdit.d.ts +20 -1
- package/dist/editors/ArchbasePasswordStrengthMeter.d.ts +22 -0
- package/dist/editors/index.d.ts +2 -0
- package/dist/index.js +5372 -5299
- package/package.json +4 -4
- package/src/editors/ArchbasePasswordEdit.tsx +92 -5
- package/src/editors/ArchbasePasswordStrengthMeter.tsx +112 -0
- package/src/editors/index.tsx +2 -0
- package/dist/archbase-components-4.0.41.tgz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archbase/components",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.43",
|
|
4
4
|
"description": "UI Components for Archbase React v3 - Form editors, data visualization, and business components",
|
|
5
5
|
"author": "Edson Martins <edsonmartins2005@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -118,9 +118,9 @@
|
|
|
118
118
|
"vis-timeline": "^7.7.3",
|
|
119
119
|
"xlsx": "^0.18.5",
|
|
120
120
|
"yet-another-react-lightbox": "^3.21.0",
|
|
121
|
-
"@archbase/core": "4.0.
|
|
122
|
-
"@archbase/data": "4.0.
|
|
123
|
-
"@archbase/layout": "4.0.
|
|
121
|
+
"@archbase/core": "4.0.43",
|
|
122
|
+
"@archbase/data": "4.0.43",
|
|
123
|
+
"@archbase/layout": "4.0.43"
|
|
124
124
|
},
|
|
125
125
|
"devDependencies": {
|
|
126
126
|
"@types/d3": "^7.4.3",
|
|
@@ -9,12 +9,17 @@ import {
|
|
|
9
9
|
} from '@mantine/core';
|
|
10
10
|
import { useForceUpdate } from '@mantine/hooks';
|
|
11
11
|
import type { CSSProperties, FocusEventHandler, ReactNode } from 'react';
|
|
12
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
12
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
13
13
|
import type { ArchbaseDataSource, DataSourceEvent, IArchbaseDataSourceBase } from '@archbase/data';
|
|
14
14
|
import { DataSourceEventNames } from '@archbase/data';
|
|
15
15
|
import { useArchbaseDidUpdate } from '@archbase/data';
|
|
16
16
|
import { useArchbaseV1V2Compatibility } from '@archbase/data';
|
|
17
|
-
import {
|
|
17
|
+
import type {
|
|
18
|
+
ArchbasePasswordPolicy,
|
|
19
|
+
ArchbasePasswordValidationResult,
|
|
20
|
+
} from '@archbase/core';
|
|
21
|
+
import { resolveArchbasePasswordPolicy, useValidationErrors, validateArchbasePassword } from '@archbase/core';
|
|
22
|
+
import { ArchbasePasswordStrengthMeter } from './ArchbasePasswordStrengthMeter';
|
|
18
23
|
|
|
19
24
|
export interface ArchbasePasswordEditProps<T, ID> {
|
|
20
25
|
/** Fonte de dados onde será atribuido o valor do edit (V1 ou V2) */
|
|
@@ -54,6 +59,24 @@ export interface ArchbasePasswordEditProps<T, ID> {
|
|
|
54
59
|
/** Referência para o componente interno */
|
|
55
60
|
innerRef?: React.RefObject<HTMLInputElement> | undefined;
|
|
56
61
|
variant?: InputVariant;
|
|
62
|
+
/**
|
|
63
|
+
* Critérios de senha forte aplicados como pré-validação no próprio componente.
|
|
64
|
+
* `true` aplica a política padrão do Archbase; um objeto permite ajustar cada critério.
|
|
65
|
+
* Quando ausente nenhuma validação de força é feita (comportamento original).
|
|
66
|
+
*/
|
|
67
|
+
passwordPolicy?: ArchbasePasswordPolicy | boolean;
|
|
68
|
+
/** Indicador se a lista de critérios deve ser exibida. Padrão: true quando há política */
|
|
69
|
+
showPasswordRequirements?: boolean;
|
|
70
|
+
/** Indicador se a barra de força deve ser exibida. Padrão: true quando há política */
|
|
71
|
+
showPasswordStrengthBar?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Quando os critérios ficam visíveis:
|
|
74
|
+
* `onFocus` (padrão) enquanto o campo tem foco ou está inválido,
|
|
75
|
+
* `always` sempre, `whenInvalid` somente quando algum critério falha.
|
|
76
|
+
*/
|
|
77
|
+
passwordRequirementsVisibility?: 'always' | 'onFocus' | 'whenInvalid';
|
|
78
|
+
/** Evento disparado a cada mudança no resultado da pré-validação */
|
|
79
|
+
onPasswordValidation?: (result: ArchbasePasswordValidationResult) => void;
|
|
57
80
|
}
|
|
58
81
|
|
|
59
82
|
export function ArchbasePasswordEdit<T, ID>({
|
|
@@ -77,6 +100,11 @@ export function ArchbasePasswordEdit<T, ID>({
|
|
|
77
100
|
onFocusEnter = () => {},
|
|
78
101
|
onChangeValue = () => {},
|
|
79
102
|
variant,
|
|
103
|
+
passwordPolicy,
|
|
104
|
+
showPasswordRequirements = true,
|
|
105
|
+
showPasswordStrengthBar = true,
|
|
106
|
+
passwordRequirementsVisibility = 'onFocus',
|
|
107
|
+
onPasswordValidation,
|
|
80
108
|
}: ArchbasePasswordEditProps<T, ID>) {
|
|
81
109
|
// 🔄 MIGRAÇÃO V1/V2: Hook de compatibilidade
|
|
82
110
|
const v1v2Compatibility = useArchbaseV1V2Compatibility<string>(
|
|
@@ -105,6 +133,22 @@ export function ArchbasePasswordEdit<T, ID>({
|
|
|
105
133
|
const [internalError, setInternalError] = useState<string | undefined>(error);
|
|
106
134
|
const forceUpdate = useForceUpdate();
|
|
107
135
|
|
|
136
|
+
// Pré-validação de senha forte: só entra em ação quando uma política é informada.
|
|
137
|
+
const resolvedPolicy = useMemo(() => resolveArchbasePasswordPolicy(passwordPolicy), [passwordPolicy]);
|
|
138
|
+
const [isFocused, setIsFocused] = useState<boolean>(false);
|
|
139
|
+
const [policyError, setPolicyError] = useState<string | undefined>(undefined);
|
|
140
|
+
|
|
141
|
+
const policyResult = useMemo(
|
|
142
|
+
() => (resolvedPolicy ? validateArchbasePassword(currentValue, resolvedPolicy) : undefined),
|
|
143
|
+
[currentValue, resolvedPolicy]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (policyResult && onPasswordValidation) {
|
|
148
|
+
onPasswordValidation(policyResult);
|
|
149
|
+
}
|
|
150
|
+
}, [policyResult]);
|
|
151
|
+
|
|
108
152
|
// ❌ REMOVIDO: Não limpar erro automaticamente quando valor muda
|
|
109
153
|
// O erro deve ser limpo apenas quando o usuário EDITA o campo (no handleChange)
|
|
110
154
|
// useEffect(() => {
|
|
@@ -208,6 +252,11 @@ useEffect(() => {
|
|
|
208
252
|
validationContext?.clearError(fieldKey);
|
|
209
253
|
}
|
|
210
254
|
|
|
255
|
+
// O erro de política reaparece no blur; enquanto digita o feedback fica na lista de critérios.
|
|
256
|
+
if (policyError) {
|
|
257
|
+
setPolicyError(undefined);
|
|
258
|
+
}
|
|
259
|
+
|
|
211
260
|
setCurrentValue((_prev) => changedValue);
|
|
212
261
|
|
|
213
262
|
if (dataSource && !dataSource.isBrowsing() && dataField && dataSource.getFieldValue(dataField) !== changedValue) {
|
|
@@ -221,12 +270,18 @@ useEffect(() => {
|
|
|
221
270
|
};
|
|
222
271
|
|
|
223
272
|
const handleOnFocusExit = (event) => {
|
|
273
|
+
setIsFocused(false);
|
|
274
|
+
// Só acusa erro depois que o usuário terminou de digitar e o campo tem conteúdo.
|
|
275
|
+
if (policyResult && currentValue) {
|
|
276
|
+
setPolicyError(policyResult.error);
|
|
277
|
+
}
|
|
224
278
|
if (onFocusExit) {
|
|
225
279
|
onFocusExit(event);
|
|
226
280
|
}
|
|
227
281
|
};
|
|
228
282
|
|
|
229
283
|
const handleOnFocusEnter = (event) => {
|
|
284
|
+
setIsFocused(true);
|
|
230
285
|
if (onFocusEnter) {
|
|
231
286
|
onFocusEnter(event);
|
|
232
287
|
}
|
|
@@ -237,10 +292,24 @@ useEffect(() => {
|
|
|
237
292
|
return readOnly || v1v2Compatibility.isReadOnly;
|
|
238
293
|
};
|
|
239
294
|
|
|
240
|
-
// Erro a ser exibido: local ou
|
|
241
|
-
const displayError = internalError || contextError;
|
|
295
|
+
// Erro a ser exibido: local, do contexto ou da política de senha
|
|
296
|
+
const displayError = internalError || contextError || policyError;
|
|
242
297
|
|
|
243
|
-
|
|
298
|
+
const shouldShowPolicyFeedback = (): boolean => {
|
|
299
|
+
if (!policyResult || isReadOnly() || disabled) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
switch (passwordRequirementsVisibility) {
|
|
303
|
+
case 'always':
|
|
304
|
+
return true;
|
|
305
|
+
case 'whenInvalid':
|
|
306
|
+
return currentValue.length > 0 && !policyResult.valid;
|
|
307
|
+
default:
|
|
308
|
+
return isFocused || (currentValue.length > 0 && !policyResult.valid);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const passwordInput = (
|
|
244
313
|
<PasswordInput
|
|
245
314
|
disabled={disabled}
|
|
246
315
|
readOnly={isReadOnly()}
|
|
@@ -263,4 +332,22 @@ useEffect(() => {
|
|
|
263
332
|
error={displayError}
|
|
264
333
|
/>
|
|
265
334
|
);
|
|
335
|
+
|
|
336
|
+
if (!resolvedPolicy) {
|
|
337
|
+
return passwordInput;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div style={{ width }}>
|
|
342
|
+
{passwordInput}
|
|
343
|
+
{shouldShowPolicyFeedback() && (
|
|
344
|
+
<ArchbasePasswordStrengthMeter
|
|
345
|
+
value={currentValue}
|
|
346
|
+
policy={resolvedPolicy}
|
|
347
|
+
showStrengthBar={showPasswordStrengthBar}
|
|
348
|
+
showRequirements={showPasswordRequirements}
|
|
349
|
+
/>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
266
353
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Box, Group, MantineSize, Progress, Stack, Text } from '@mantine/core';
|
|
2
|
+
import { IconCheck, IconX } from '@tabler/icons-react';
|
|
3
|
+
import type { CSSProperties } from 'react';
|
|
4
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
5
|
+
import type {
|
|
6
|
+
ArchbasePasswordPolicy,
|
|
7
|
+
ArchbasePasswordStrength,
|
|
8
|
+
ArchbasePasswordValidationResult,
|
|
9
|
+
} from '@archbase/core';
|
|
10
|
+
import {
|
|
11
|
+
getArchbasePasswordStrengthLabel,
|
|
12
|
+
resolveArchbasePasswordPolicy,
|
|
13
|
+
validateArchbasePassword,
|
|
14
|
+
} from '@archbase/core';
|
|
15
|
+
|
|
16
|
+
export interface ArchbasePasswordStrengthMeterProps {
|
|
17
|
+
/** Senha a ser avaliada */
|
|
18
|
+
value?: string;
|
|
19
|
+
/** Critérios de senha forte. `true` aplica a política padrão do Archbase */
|
|
20
|
+
policy?: ArchbasePasswordPolicy | boolean;
|
|
21
|
+
/** Indicador se a barra de força deve ser exibida */
|
|
22
|
+
showStrengthBar?: boolean;
|
|
23
|
+
/** Indicador se a lista de critérios deve ser exibida */
|
|
24
|
+
showRequirements?: boolean;
|
|
25
|
+
/** Exibe somente os critérios ainda não atendidos */
|
|
26
|
+
onlyUnmetRequirements?: boolean;
|
|
27
|
+
/** Tamanho do texto dos critérios */
|
|
28
|
+
size?: MantineSize;
|
|
29
|
+
/** Evento disparado sempre que o resultado da validação muda */
|
|
30
|
+
onValidationChange?: (result: ArchbasePasswordValidationResult) => void;
|
|
31
|
+
/** Estilo do container */
|
|
32
|
+
style?: CSSProperties;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const STRENGTH_COLORS: Record<ArchbasePasswordStrength, string> = {
|
|
36
|
+
empty: 'gray',
|
|
37
|
+
weak: 'red',
|
|
38
|
+
fair: 'orange',
|
|
39
|
+
good: 'yellow',
|
|
40
|
+
strong: 'teal',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function ArchbasePasswordStrengthMeter({
|
|
44
|
+
value = '',
|
|
45
|
+
policy = true,
|
|
46
|
+
showStrengthBar = true,
|
|
47
|
+
showRequirements = true,
|
|
48
|
+
onlyUnmetRequirements = false,
|
|
49
|
+
size = 'xs',
|
|
50
|
+
onValidationChange,
|
|
51
|
+
style,
|
|
52
|
+
}: ArchbasePasswordStrengthMeterProps) {
|
|
53
|
+
const resolvedPolicy = useMemo(() => resolveArchbasePasswordPolicy(policy), [policy]);
|
|
54
|
+
|
|
55
|
+
const result = useMemo(
|
|
56
|
+
() => (resolvedPolicy ? validateArchbasePassword(value, resolvedPolicy) : undefined),
|
|
57
|
+
[value, resolvedPolicy]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Notifica apenas quando o resultado realmente muda, evitando loops de render no consumidor.
|
|
61
|
+
const lastNotifiedRef = useRef<string | undefined>(undefined);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!result || !onValidationChange) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const signature = `${result.valid}|${result.score}|${result.unmetRequirements
|
|
67
|
+
.map((requirement) => requirement.key)
|
|
68
|
+
.join(',')}`;
|
|
69
|
+
if (lastNotifiedRef.current !== signature) {
|
|
70
|
+
lastNotifiedRef.current = signature;
|
|
71
|
+
onValidationChange(result);
|
|
72
|
+
}
|
|
73
|
+
}, [result, onValidationChange]);
|
|
74
|
+
|
|
75
|
+
if (!result) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const color = STRENGTH_COLORS[result.strength];
|
|
80
|
+
const requirements = onlyUnmetRequirements ? result.unmetRequirements : result.requirements;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Stack gap={4} mt={6} style={style}>
|
|
84
|
+
{showStrengthBar && (
|
|
85
|
+
<Box>
|
|
86
|
+
<Progress value={result.score} color={color} size="sm" radius="xl" />
|
|
87
|
+
{result.strength !== 'empty' && (
|
|
88
|
+
<Text size={size} c={color} mt={2}>
|
|
89
|
+
{getArchbasePasswordStrengthLabel(result.strength)}
|
|
90
|
+
</Text>
|
|
91
|
+
)}
|
|
92
|
+
</Box>
|
|
93
|
+
)}
|
|
94
|
+
{showRequirements && requirements.length > 0 && (
|
|
95
|
+
<Stack gap={2}>
|
|
96
|
+
{requirements.map((requirement) => (
|
|
97
|
+
<Group key={requirement.key} gap={6} wrap="nowrap" align="center">
|
|
98
|
+
{requirement.satisfied ? (
|
|
99
|
+
<IconCheck size={14} color="var(--mantine-color-teal-6)" />
|
|
100
|
+
) : (
|
|
101
|
+
<IconX size={14} color="var(--mantine-color-red-6)" />
|
|
102
|
+
)}
|
|
103
|
+
<Text size={size} c={requirement.satisfied ? 'teal' : 'dimmed'}>
|
|
104
|
+
{requirement.label}
|
|
105
|
+
</Text>
|
|
106
|
+
</Group>
|
|
107
|
+
))}
|
|
108
|
+
</Stack>
|
|
109
|
+
)}
|
|
110
|
+
</Stack>
|
|
111
|
+
);
|
|
112
|
+
}
|
package/src/editors/index.tsx
CHANGED
|
@@ -63,6 +63,8 @@ export type { ArchbaseNumberEditProps } from './ArchbaseNumberEdit';
|
|
|
63
63
|
|
|
64
64
|
export { ArchbasePasswordEdit } from './ArchbasePasswordEdit';
|
|
65
65
|
export type { ArchbasePasswordEditProps } from './ArchbasePasswordEdit';
|
|
66
|
+
export { ArchbasePasswordStrengthMeter } from './ArchbasePasswordStrengthMeter';
|
|
67
|
+
export type { ArchbasePasswordStrengthMeterProps } from './ArchbasePasswordStrengthMeter';
|
|
66
68
|
|
|
67
69
|
export { ArchbaseRadioGroup } from './ArchbaseRadioGroup';
|
|
68
70
|
export type { ArchbaseRadioGroupProps } from './ArchbaseRadioGroup';
|
|
Binary file
|