@controleonline/ui-default 1.0.272 → 1.0.274
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/.github/workflows/publish-npm.yml +91 -0
- package/AGENTS.md +1 -1
- package/docs/technical/DefaultTable.md +70 -1
- package/package.json +22 -22
- package/src/react/components/address/DefaultAddress.js +20 -9
- package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
- package/src/react/components/address/LatLonReadonlyFields.js +2 -2
- package/src/react/components/address/defaultAddressStyles.js +13 -1
- package/src/react/components/files/DefaultFile.js +2 -2
- package/src/react/components/files/DefaultFileColumn.js +1 -1
- package/src/react/components/filters/DefaultColumnFilter.js +2 -7
- package/src/react/components/filters/DefaultExternalFilters.js +4 -11
- package/src/react/components/filters/filterValue.js +65 -0
- package/src/react/components/help/DefaultTooltip.js +73 -63
- package/src/react/components/help/DefaultTooltip.styles.js +23 -23
- package/src/react/components/inputs/DefaultInput.js +301 -210
- package/src/react/components/inputs/defaultInputUtils.js +193 -170
- package/src/react/components/table/DefaultColumnMenu.js +127 -109
- package/src/react/components/table/DefaultTable.js +43 -92
- package/src/react/components/table/DefaultTable.styles.js +10 -4
- package/src/react/components/table/DefaultTable.utils.js +38 -4
- package/src/react/components/table/DefaultTableRows.js +324 -153
- package/src/react/components/table/DefaultTableToolbar.js +17 -7
- package/src/react/components/table/DefaultTableView.js +118 -0
- package/src/react/components/upload/DefaultUpload.js +6 -36
- package/src/react/components/upload/DefaultUpload.styles.js +5 -5
- package/src/react/components/upload/DefaultUploadAttachmentsList.js +11 -3
- package/src/react/hooks/usePostalCodeLookup.js +15 -1
- package/src/react/services/addressFormUtils.js +104 -38
- package/src/react/services/addressGeo.js +6 -3
- package/src/react/utils/tableVisibleColumnsPreferences.js +84 -3
- package/src/tests/react/components/files/DefaultFile.test.js +16 -0
- package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
- package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
- package/src/tests/react/components/table/DefaultTable.test.js +37 -0
- package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
- package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
- package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
- package/src/tests/react/components/table/DefaultTableToolbar.waiter.test.js +42 -0
- package/src/tests/react/components/upload/defaultUploadLibrary.test.js +31 -9
- package/src/tests/react/filters/filterValue.test.js +46 -0
- package/src/tests/react/hooks/usePostalCodeLookup.test.js +67 -0
- package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
- package/src/tests/react/services/addressFormUtils.test.js +6 -3
- package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +12 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
name: Publish Package to npmjs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: write
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.repository }}-npm-release
|
|
12
|
+
cancel-in-progress: false
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
publish:
|
|
16
|
+
if: ${{ github.repository_owner == 'ControleOnline' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) }}
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
ref: ${{ github.ref }}
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
- name: Validate package metadata and create version tag
|
|
24
|
+
id: release
|
|
25
|
+
env:
|
|
26
|
+
REF_TYPE: ${{ github.ref_type }}
|
|
27
|
+
REF_NAME: ${{ github.ref_name }}
|
|
28
|
+
EXPECTED_REPOSITORY: ${{ github.repository }}
|
|
29
|
+
run: |
|
|
30
|
+
node <<'NODE'
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const cp = require('node:child_process');
|
|
33
|
+
const pkg = require('./package.json');
|
|
34
|
+
const expectedUrl = `https://github.com/${process.env.EXPECTED_REPOSITORY}`;
|
|
35
|
+
if (!/^\d+\.\d+\.\d+$/.test(String(pkg.version))) throw new Error(`Expected stable X.Y.Z, got ${pkg.version}`);
|
|
36
|
+
if (pkg.repository?.url !== expectedUrl) throw new Error(`repository.url must be ${expectedUrl}; got ${pkg.repository?.url}`);
|
|
37
|
+
const sourceSha = cp.execFileSync('git', ['rev-parse', 'HEAD'], {encoding:'utf8'}).trim();
|
|
38
|
+
let tag = '';
|
|
39
|
+
if (process.env.REF_TYPE === 'tag') {
|
|
40
|
+
tag = process.env.REF_NAME;
|
|
41
|
+
if (tag !== `v${pkg.version}`) throw new Error(`Tag ${tag} does not match package version ${pkg.version}`);
|
|
42
|
+
} else {
|
|
43
|
+
let previous = '';
|
|
44
|
+
try { previous = JSON.parse(cp.execFileSync('git', ['show', `${sourceSha}^:package.json`], {encoding:'utf8'})).version || ''; } catch {}
|
|
45
|
+
if (previous === pkg.version) { fs.appendFileSync(process.env.GITHUB_OUTPUT, 'release_tag=\n'); process.exit(0); }
|
|
46
|
+
const parts = String(pkg.version).split('.').map(Number);
|
|
47
|
+
if (previous) {
|
|
48
|
+
const old = String(previous).split('.').map(Number);
|
|
49
|
+
let cmp = 0; for (let i=0;i<3;i++) { if ((parts[i]||0)!==(old[i]||0)) { cmp=(parts[i]||0)>(old[i]||0)?1:-1; break; } }
|
|
50
|
+
if (cmp <= 0) throw new Error(`Version ${pkg.version} must be greater than first-parent version ${previous}`);
|
|
51
|
+
}
|
|
52
|
+
tag = `v${pkg.version}`;
|
|
53
|
+
const existing = cp.spawnSync('git', ['ls-remote', '--exit-code', '--refs', 'origin', `refs/tags/${tag}`], {encoding:'utf8'});
|
|
54
|
+
if (existing.status === 0) {
|
|
55
|
+
cp.execFileSync('git', ['fetch', 'origin', `refs/tags/${tag}:refs/tags/${tag}`], {stdio:'inherit'});
|
|
56
|
+
const tagged = cp.execFileSync('git', ['rev-parse', `${tag}^{commit}`], {encoding:'utf8'}).trim();
|
|
57
|
+
if (tagged !== sourceSha) throw new Error(`Immutable tag ${tag} already points at ${tagged}, not ${sourceSha}`);
|
|
58
|
+
} else {
|
|
59
|
+
cp.execFileSync('git', ['config', 'user.name', 'github-actions[bot]']);
|
|
60
|
+
cp.execFileSync('git', ['config', 'user.email', '41898282+github-actions[bot]@users.noreply.github.com']);
|
|
61
|
+
cp.execFileSync('git', ['tag', '-a', tag, sourceSha, '-m', `Release ${pkg.name}@${pkg.version}`], {stdio:'inherit'});
|
|
62
|
+
cp.execFileSync('git', ['push', 'origin', `refs/tags/${tag}`], {stdio:'inherit'});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `release_tag=${tag}\n`);
|
|
66
|
+
console.log(`Validated ${pkg.name}@${pkg.version} from ${sourceSha}; release tag ${tag}`);
|
|
67
|
+
NODE
|
|
68
|
+
- uses: actions/setup-node@v4
|
|
69
|
+
if: steps.release.outputs.release_tag != ''
|
|
70
|
+
with:
|
|
71
|
+
node-version: 24
|
|
72
|
+
- name: Ensure npm 11.5.1+ (OIDC support)
|
|
73
|
+
if: steps.release.outputs.release_tag != ''
|
|
74
|
+
run: |
|
|
75
|
+
npm install --global npm@latest
|
|
76
|
+
npm --version
|
|
77
|
+
- name: Skip if version is already on npm
|
|
78
|
+
id: registry
|
|
79
|
+
if: steps.release.outputs.release_tag != ''
|
|
80
|
+
run: |
|
|
81
|
+
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
82
|
+
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
83
|
+
if npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version >/dev/null 2>&1; then
|
|
84
|
+
echo 'published=true' >> "$GITHUB_OUTPUT"
|
|
85
|
+
echo "$PACKAGE_NAME@$PACKAGE_VERSION is already published; skipping duplicate publish"
|
|
86
|
+
else
|
|
87
|
+
echo 'published=false' >> "$GITHUB_OUTPUT"
|
|
88
|
+
fi
|
|
89
|
+
- name: Publish to npmjs with trusted publishing
|
|
90
|
+
if: steps.release.outputs.release_tag != '' && steps.registry.outputs.published != 'true'
|
|
91
|
+
run: npm publish --access public --loglevel verbose
|
package/AGENTS.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| Página | O que documenta |
|
|
18
18
|
| --- | --- |
|
|
19
19
|
| [Índice Default*](https://github.com/ControleOnline/ui-default/wiki/Default-Components) | Mapa da família e arquitetura |
|
|
20
|
-
| [DefaultTable](https://github.com/ControleOnline/ui-default/wiki/DefaultTable) | Listagens, store contract, view modes |
|
|
20
|
+
| [DefaultTable](https://github.com/ControleOnline/ui-default/wiki/DefaultTable) | Listagens, store contract, view modes, **hydrate único de filtros** (`useDefaultTableStoreSync`, anti #185) |
|
|
21
21
|
| [DefaultForm](https://github.com/ControleOnline/ui-default/wiki/DefaultForm) | Create/edit genérico |
|
|
22
22
|
| [DefaultInput e DefaultSelect](https://github.com/ControleOnline/ui-default/wiki/DefaultInput-e-DefaultSelect) | Campos e listas |
|
|
23
23
|
| [Contrato de colunas](https://github.com/ControleOnline/ui-default/wiki/Contrato-de-colunas) | Flags e serialização |
|
|
@@ -188,16 +188,85 @@ Com colunas só via prop (store ainda sem columns):
|
|
|
188
188
|
/>
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
|
|
192
|
+
## Sincronização com o store (`useDefaultTableStoreSync`)
|
|
193
|
+
|
|
194
|
+
Hook interno que mantém props controladas do `DefaultTable` alinhadas ao slice Zustand **sem depender da identidade do objeto `store`** (Zustand troca a referência a cada commit).
|
|
195
|
+
|
|
196
|
+
**Arquivo:** `src/react/components/table/useDefaultTableStoreSync.js`
|
|
197
|
+
**Testes:** `src/tests/react/components/table/useDefaultTableStoreSync.test.js`
|
|
198
|
+
|
|
199
|
+
### O que sincroniza
|
|
200
|
+
|
|
201
|
+
| Efeito | Origem → destino | Guard anti-loop |
|
|
202
|
+
| --- | --- | --- |
|
|
203
|
+
| Colunas | prop `columns` → `actions.setColumns` / `getters.columns` | só se o store ainda não tiver colunas |
|
|
204
|
+
| Itens controlados | prop `data` → `actions.setItems` | mesma referência de array (`lastPublishedItemsRef`) |
|
|
205
|
+
| Colunas visíveis | `localStorage` (escopo empresa/store/rota) → `setVisibleColumns` | sanitização por colunas atuais |
|
|
206
|
+
| Filtros persistidos | `localStorage` → `setFilters` | **hydrate uma vez por scopeKey** + `areTableFiltersEqual` |
|
|
207
|
+
| Configs | `defaultTableConfigs` → `setConfigs` | assinatura estável (`defaultTableConfigsSignature`) |
|
|
208
|
+
|
|
209
|
+
Helpers exportados: `publishStoreValue`, `assignGetterValue`, `areTableFiltersEqual`.
|
|
210
|
+
|
|
211
|
+
### Preferências de filtro — regra de hydrate único
|
|
212
|
+
|
|
213
|
+
Filtros de tabela são lidos de `resolveStoredTableFiltersPreference(tablePreferenceScope)` e aplicados via `sanitizeTableFiltersPreference`.
|
|
214
|
+
|
|
215
|
+
**`scopeKey`** (estável):
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
companyKey :: storeKey :: routeKey :: colunas(key|name)|...
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Regras canônicas (app-community#448 / ui-orders OrderHistoryPage):
|
|
222
|
+
|
|
223
|
+
1. Hydrate de filtros persistidos ocorre **no máximo uma vez** por `scopeKey` (`lastHydratedFiltersKeyRef`).
|
|
224
|
+
2. **Não** reagir a mudanças de `storeFilters` após o hydrate — perseguir o store recria o payload e gera React **#185** (Maximum update depth exceeded).
|
|
225
|
+
3. Antes de `setFilters`, comparar com `areTableFiltersEqual` (JSON estável). Se já for equivalente, só marca o `scopeKey` e sai.
|
|
226
|
+
4. Toggle típico que quebrava: período `today → all → today` em `OrderHistoryPage` (filtro externo de data + DefaultTable). O guard em `useOrderHistoryFilters` (`areHistoryFiltersEqual`) **não basta** se o loop nascer no hydrate do DefaultTable.
|
|
227
|
+
|
|
228
|
+
```mermaid
|
|
229
|
+
flowchart TD
|
|
230
|
+
A[DefaultTable mount / columns ready] --> B{stored filters?}
|
|
231
|
+
B -->|não| C[marca scopeKey]
|
|
232
|
+
B -->|sim| D[sanitize]
|
|
233
|
+
D --> E{scopeKey já hydrated?}
|
|
234
|
+
E -->|sim| F[return — não chase storeFilters]
|
|
235
|
+
E -->|não| G{areTableFiltersEqual?}
|
|
236
|
+
G -->|sim| C
|
|
237
|
+
G -->|não| H[setFilters + marca scopeKey]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Por que não colocar `storeFilters` nas deps
|
|
241
|
+
|
|
242
|
+
Incluir `storeFilters` no array de dependências do efeito de hydrate faz:
|
|
243
|
+
|
|
244
|
+
`setFilters` → nova referência no store → efeito roda de novo → `setFilters` → **#185**.
|
|
245
|
+
|
|
246
|
+
A correção remove `storeFilters` das deps e usa `storeFiltersRef` só para a comparação de igualdade no momento do hydrate.
|
|
247
|
+
|
|
248
|
+
### Visões (`APP_TYPE`)
|
|
249
|
+
|
|
250
|
+
`ui-default` **não** conhece domínio de pedido/CRM/POS. Qualquer tela que use `DefaultTable` + filtros de período (ex.: histórico de pedidos em `ui-orders`, listagens CRM) herda este contrato. O módulo consumidor não deve reaplicar filtros persistidos em todo change do store; o orquestrador já faz o hydrate único.
|
|
251
|
+
|
|
252
|
+
### Testes mínimos esperados
|
|
253
|
+
|
|
254
|
+
- Publicação de `data` uma vez por referência de array (Devices / #185 histórico).
|
|
255
|
+
- Hydrate de filtros uma vez; após churn de `storeFilters` (simular today→all→today), `setFilters` continua 1×.
|
|
256
|
+
- `areTableFiltersEqual` trata payloads de período equivalentes como iguais.
|
|
257
|
+
|
|
191
258
|
## Limites e cuidados
|
|
192
259
|
|
|
193
260
|
- Filhos dependem de `configs` no store: não renderize Body isolado sem o orquestrador.
|
|
194
261
|
- Preferências são por **empresa + store + rota**: mudou empresa ou rota, o escopo muda.
|
|
195
262
|
- `stableSerialize` evita loops de `setConfigs`.
|
|
263
|
+
- Hydrate de filtros persistidos é **único por scopeKey** — não recoloque `storeFilters` nas deps do efeito de hydrate (React #185; ver seção `useDefaultTableStoreSync`).
|
|
196
264
|
- List stores com escopo de empresa (`categories`, `paymentType`, `wallet`) recebem `company`/`people` automaticamente nos loads de select/coluna.
|
|
197
265
|
|
|
198
266
|
## Arquivos relacionados
|
|
199
267
|
|
|
200
268
|
- `DefaultTable.js`, `DefaultTable.utils.js`, `DefaultTable.styles.js`
|
|
201
|
-
- Hooks: `useDefaultTablePagination.js`, `useDefaultTableSorting.js`, `useDefaultTableTheme.js
|
|
269
|
+
- Hooks: `useDefaultTablePagination.js`, `useDefaultTableSorting.js`, `useDefaultTableTheme.js`, **`useDefaultTableStoreSync.js`**
|
|
202
270
|
- Preferências: `src/react/utils/tableVisibleColumnsPreferences`
|
|
203
271
|
- Testes: `src/tests/react/utils/tableVisibleColumnsPreferences.test.js`
|
|
272
|
+
- Testes store sync: `src/tests/react/components/table/useDefaultTableStoreSync.test.js`
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@controleonline/ui-default",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"author": "ControleOnline",
|
|
5
|
-
"description": "ControleOnline Default Quasar UI Component",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"main": "src/react/index.js",
|
|
8
|
-
"scripts": {},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "https://github.com/ControleOnline/
|
|
12
|
-
},
|
|
13
|
-
"bugs": "",
|
|
14
|
-
"homepage": "https://www.controleonline.com/devs/plugins",
|
|
15
|
-
"browserslist": [
|
|
16
|
-
"last 1 version, not dead, ie >= 11"
|
|
17
|
-
],
|
|
18
|
-
"dependencies": {
|
|
19
|
-
"@controleonline/ui-common": "*",
|
|
20
|
-
"react-native-vector-icons": "^10.3.0"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@controleonline/ui-default",
|
|
3
|
+
"version": "1.0.274",
|
|
4
|
+
"author": "ControleOnline",
|
|
5
|
+
"description": "ControleOnline Default Quasar UI Component",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "src/react/index.js",
|
|
8
|
+
"scripts": {},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/ControleOnline/ui-default"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "",
|
|
14
|
+
"homepage": "https://www.controleonline.com/devs/plugins",
|
|
15
|
+
"browserslist": [
|
|
16
|
+
"last 1 version, not dead, ie >= 11"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@controleonline/ui-common": "*",
|
|
20
|
+
"react-native-vector-icons": "^10.3.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -16,12 +16,11 @@ import {
|
|
|
16
16
|
} from '../../services/addressGeo';
|
|
17
17
|
import {
|
|
18
18
|
buildMapMarkerPayload,
|
|
19
|
-
formatCepMask,
|
|
20
19
|
getCurrentCoordinates,
|
|
20
|
+
GEOCODE_MISS_MESSAGE,
|
|
21
21
|
hasCoordinates,
|
|
22
22
|
hydrateAddressFromRow,
|
|
23
23
|
mergePostalCodeData,
|
|
24
|
-
normalizeCepDigits,
|
|
25
24
|
onlyDigits,
|
|
26
25
|
parseOptionalCoordinate,
|
|
27
26
|
} from '../../services/addressFormUtils';
|
|
@@ -58,6 +57,8 @@ export default function DefaultAddress({
|
|
|
58
57
|
loadingCep,
|
|
59
58
|
cepError,
|
|
60
59
|
setCepError,
|
|
60
|
+
geocodeMiss,
|
|
61
|
+
setGeocodeMiss,
|
|
61
62
|
onCepBlur,
|
|
62
63
|
runLookup,
|
|
63
64
|
cancelPending,
|
|
@@ -133,10 +134,9 @@ export default function DefaultAddress({
|
|
|
133
134
|
(key, value) => {
|
|
134
135
|
setForm(prev => {
|
|
135
136
|
let nextValue = value;
|
|
136
|
-
if (key === '
|
|
137
|
-
nextValue = normalizeCepDigits(value);
|
|
138
|
-
} else if (key === 'latitude' || key === 'longitude') {
|
|
137
|
+
if (key === 'latitude' || key === 'longitude') {
|
|
139
138
|
// Keep raw string while typing so decimals/signs are not truncated mid-edit.
|
|
139
|
+
// Persist number only when the full token parses; otherwise keep the draft string.
|
|
140
140
|
const parsed = parseOptionalCoordinate(value);
|
|
141
141
|
const trimmed = String(value ?? '').trim();
|
|
142
142
|
const looksComplete =
|
|
@@ -150,6 +150,10 @@ export default function DefaultAddress({
|
|
|
150
150
|
parsed != null && looksComplete ? parsed : value;
|
|
151
151
|
}
|
|
152
152
|
const next = {...prev, [key]: nextValue};
|
|
153
|
+
// Troca de CEP: número do imóvel anterior não se aplica (#746)
|
|
154
|
+
if (key === 'cep' && onlyDigits(value) !== onlyDigits(prev.cep)) {
|
|
155
|
+
next.number = '';
|
|
156
|
+
}
|
|
153
157
|
onFormChange?.(next);
|
|
154
158
|
return next;
|
|
155
159
|
});
|
|
@@ -159,10 +163,11 @@ export default function DefaultAddress({
|
|
|
159
163
|
if (key === 'cep') {
|
|
160
164
|
setCepError(null);
|
|
161
165
|
setError(null);
|
|
166
|
+
setGeocodeMiss(false);
|
|
162
167
|
setCoordsManualEdit(false);
|
|
163
168
|
}
|
|
164
169
|
},
|
|
165
|
-
[onFormChange, setCepError],
|
|
170
|
+
[onFormChange, setCepError, setGeocodeMiss],
|
|
166
171
|
);
|
|
167
172
|
|
|
168
173
|
const onSelectCountry = useCallback(async item => {
|
|
@@ -269,8 +274,13 @@ export default function DefaultAddress({
|
|
|
269
274
|
keyboardShouldPersistTaps="handled">
|
|
270
275
|
<View style={[styles.formPane, isDesktop && styles.formPaneDesktop]}>
|
|
271
276
|
{displayError ? <Text style={styles.error}>{displayError}</Text> : null}
|
|
277
|
+
{geocodeMiss ? (
|
|
278
|
+
<Text testID="address-geocode-miss" style={styles.hint}>
|
|
279
|
+
{GEOCODE_MISS_MESSAGE}
|
|
280
|
+
</Text>
|
|
281
|
+
) : null}
|
|
272
282
|
<Text style={styles.hint}>
|
|
273
|
-
Informe o CEP (8 dígitos) para autopreencher via ERP.
|
|
283
|
+
Informe o CEP (8 dígitos) para autopreencher via ERP. Complemento/apelido são preservados; o número é limpo ao trocar o CEP.
|
|
274
284
|
</Text>
|
|
275
285
|
|
|
276
286
|
<Field label="Apelido">
|
|
@@ -287,7 +297,7 @@ export default function DefaultAddress({
|
|
|
287
297
|
<TextInput
|
|
288
298
|
testID="address-cep-input"
|
|
289
299
|
style={[styles.input, styles.flex]}
|
|
290
|
-
value={
|
|
300
|
+
value={form.cep}
|
|
291
301
|
onChangeText={v => onChange('cep', v)}
|
|
292
302
|
onBlur={onCepBlur}
|
|
293
303
|
keyboardType="number-pad"
|
|
@@ -399,7 +409,7 @@ export default function DefaultAddress({
|
|
|
399
409
|
form={form}
|
|
400
410
|
styles={styles}
|
|
401
411
|
Field={Field}
|
|
402
|
-
editable={!hasCoordinates(form) || coordsManualEdit}
|
|
412
|
+
editable={!hasCoordinates(form) || geocodeMiss || coordsManualEdit}
|
|
403
413
|
onChange={onChange}
|
|
404
414
|
/>
|
|
405
415
|
|
|
@@ -433,3 +443,4 @@ function Field({label, children, style = null}) {
|
|
|
433
443
|
</View>
|
|
434
444
|
);
|
|
435
445
|
}
|
|
446
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Explicit Latitude/Longitude inputs for DefaultAddress (app-community#360).
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import {Text, TextInput, View} from 'react-native';
|
|
6
|
+
import styles from './DefaultAddress.styles';
|
|
7
|
+
import {parseOptionalCoordinate} from '../../services/addressFormUtils';
|
|
8
|
+
|
|
9
|
+
function Field({label, children, style = null}) {
|
|
10
|
+
return (
|
|
11
|
+
<View style={[styles.field, style]}>
|
|
12
|
+
<Text style={styles.label}>{label}</Text>
|
|
13
|
+
{children}
|
|
14
|
+
</View>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function DefaultAddressCoordFields({form, onChange}) {
|
|
19
|
+
return (
|
|
20
|
+
<View style={styles.coordRow}>
|
|
21
|
+
<View style={styles.coordHalf}>
|
|
22
|
+
<Field label="Latitude">
|
|
23
|
+
<TextInput
|
|
24
|
+
style={styles.input}
|
|
25
|
+
value={
|
|
26
|
+
form.latitude === null || form.latitude === undefined
|
|
27
|
+
? ''
|
|
28
|
+
: String(form.latitude)
|
|
29
|
+
}
|
|
30
|
+
onChangeText={v => onChange('latitude', parseOptionalCoordinate(v))}
|
|
31
|
+
placeholder="-23.5505"
|
|
32
|
+
keyboardType="decimal-pad"
|
|
33
|
+
accessibilityLabel="Latitude"
|
|
34
|
+
testID="address-latitude-input"
|
|
35
|
+
/>
|
|
36
|
+
</Field>
|
|
37
|
+
</View>
|
|
38
|
+
<View style={styles.coordHalf}>
|
|
39
|
+
<Field label="Longitude">
|
|
40
|
+
<TextInput
|
|
41
|
+
style={styles.input}
|
|
42
|
+
value={
|
|
43
|
+
form.longitude === null || form.longitude === undefined
|
|
44
|
+
? ''
|
|
45
|
+
: String(form.longitude)
|
|
46
|
+
}
|
|
47
|
+
onChangeText={v =>
|
|
48
|
+
onChange('longitude', parseOptionalCoordinate(v))
|
|
49
|
+
}
|
|
50
|
+
placeholder="-46.6333"
|
|
51
|
+
keyboardType="decimal-pad"
|
|
52
|
+
accessibilityLabel="Longitude"
|
|
53
|
+
testID="address-longitude-input"
|
|
54
|
+
/>
|
|
55
|
+
</Field>
|
|
56
|
+
</View>
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -3,8 +3,8 @@ import {View, TextInput} from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Latitude / longitude fields.
|
|
6
|
-
* Read-only
|
|
7
|
-
*
|
|
6
|
+
* Read-only after a successful geocode; editable when Nominatim misses
|
|
7
|
+
* so the user can type coordinates and drop a pin.
|
|
8
8
|
*/
|
|
9
9
|
export default function LatLonReadonlyFields({
|
|
10
10
|
form,
|
|
@@ -133,4 +133,16 @@ export const styles = StyleSheet.create({
|
|
|
133
133
|
btnPrimaryText: {color: '#fff', fontWeight: '600'},
|
|
134
134
|
btnSecondaryText: {color: '#475569', fontWeight: '600'},
|
|
135
135
|
btnDisabled: {opacity: 0.6},
|
|
136
|
-
|
|
136
|
+
|
|
137
|
+
coordRow: {
|
|
138
|
+
flexDirection: 'row',
|
|
139
|
+
gap: 12,
|
|
140
|
+
},
|
|
141
|
+
coordField: {
|
|
142
|
+
flex: 1,
|
|
143
|
+
},
|
|
144
|
+
inputReadonly: {
|
|
145
|
+
backgroundColor: '#F1F5F9',
|
|
146
|
+
color: '#475569',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
@@ -4,8 +4,8 @@ import {useStore} from '@store';
|
|
|
4
4
|
import {resolveDefaultFileSource} from '@controleonline/ui-common/src/react/utils/fileUrl';
|
|
5
5
|
|
|
6
6
|
const resolveCompanyFromStore = peopleGetters => {
|
|
7
|
-
if (peopleGetters?.
|
|
8
|
-
return peopleGetters.
|
|
7
|
+
if (peopleGetters?.mainCompany?.id) {
|
|
8
|
+
return peopleGetters.mainCompany;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
if (peopleGetters?.currentCompany?.id) {
|
|
@@ -19,7 +19,7 @@ const ICON_BY_KIND = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
const resolveCompanyFromStore = peopleGetters => {
|
|
22
|
-
if (peopleGetters?.
|
|
22
|
+
if (peopleGetters?.mainCompany?.id) return peopleGetters.mainCompany;
|
|
23
23
|
if (peopleGetters?.currentCompany?.id) return peopleGetters.currentCompany;
|
|
24
24
|
return null;
|
|
25
25
|
};
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import CompactFilterSelector from './CompactFilterSelector';
|
|
14
14
|
import DateShortcutFilter from './DateShortcutFilter';
|
|
15
15
|
import { resolveNextDateFilterValue } from './dateFilterSelection';
|
|
16
|
+
import { isFilledFilterValue } from './filterValue';
|
|
16
17
|
import useDefaultTableTheme from '@controleonline/ui-default/src/react/components/table/useDefaultTableTheme';
|
|
17
18
|
import {
|
|
18
19
|
normalizeCollectionItems,
|
|
@@ -61,13 +62,7 @@ const DefaultColumnFilter = ({
|
|
|
61
62
|
|
|
62
63
|
const currentFilters = store?.getters?.filters || {};
|
|
63
64
|
const nextFilters = { ...(currentFilters || {}) };
|
|
64
|
-
|
|
65
|
-
value === null ||
|
|
66
|
-
value === undefined ||
|
|
67
|
-
normalizeText(value) === '' ||
|
|
68
|
-
(Array.isArray(value) && value.length === 0);
|
|
69
|
-
|
|
70
|
-
if (isEmpty) delete nextFilters[nextFieldName];
|
|
65
|
+
if (!isFilledFilterValue(value)) delete nextFilters[nextFieldName];
|
|
71
66
|
else nextFilters[nextFieldName] = value;
|
|
72
67
|
|
|
73
68
|
const columns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import CompactFilterSelector from './CompactFilterSelector';
|
|
23
23
|
import DateShortcutFilter from './DateShortcutFilter';
|
|
24
24
|
import { resolveNextDateFilterValue } from './dateFilterSelection';
|
|
25
|
+
import { isFilledFilterValue, omitEmptyFilters } from './filterValue';
|
|
25
26
|
import {
|
|
26
27
|
persistTableFiltersPreference,
|
|
27
28
|
resolveDefaultTablePreferenceScope,
|
|
@@ -108,15 +109,6 @@ const normalizeFilterValue = value => {
|
|
|
108
109
|
return normalizeText(value);
|
|
109
110
|
};
|
|
110
111
|
|
|
111
|
-
const isFilledFilterValue = value => {
|
|
112
|
-
if (Array.isArray(value)) return value.length > 0;
|
|
113
|
-
if (value && typeof value === 'object') {
|
|
114
|
-
return Object.values(value).some(isFilledFilterValue);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return normalizeText(value) !== '';
|
|
118
|
-
};
|
|
119
|
-
|
|
120
112
|
const resolveDateState = filterValue => {
|
|
121
113
|
if (!filterValue || typeof filterValue !== 'object') {
|
|
122
114
|
return {
|
|
@@ -222,10 +214,11 @@ const DefaultExternalFilters = ({
|
|
|
222
214
|
}, [activeCount, onActiveCountChange]);
|
|
223
215
|
|
|
224
216
|
const applyFilters = useCallback(nextFilters => {
|
|
225
|
-
const resolvedFilters =
|
|
217
|
+
const resolvedFilters = omitEmptyFilters(
|
|
226
218
|
nextFilters && typeof nextFilters === 'object' && !Array.isArray(nextFilters)
|
|
227
219
|
? nextFilters
|
|
228
|
-
: {}
|
|
220
|
+
: {},
|
|
221
|
+
);
|
|
229
222
|
const columns = Array.isArray(externalFilterStore?.getters?.columns)
|
|
230
223
|
? externalFilterStore.getters.columns
|
|
231
224
|
: [];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const EMPTY_SHORTCUTS = new Set(['', 'all', 'none', '*']);
|
|
2
|
+
|
|
3
|
+
const normalizeText = value => String(value ?? '').trim();
|
|
4
|
+
|
|
5
|
+
export const isFilledFilterValue = value => {
|
|
6
|
+
if (value === null || value === undefined) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return value.some(isFilledFilterValue);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof value === 'object') {
|
|
15
|
+
const shortcut = normalizeText(value.shortcut).toLowerCase();
|
|
16
|
+
const range = value.customRange && typeof value.customRange === 'object'
|
|
17
|
+
? value.customRange
|
|
18
|
+
: null;
|
|
19
|
+
const rangeFrom = normalizeText(
|
|
20
|
+
range?.from ?? value.from ?? value.start ?? value.after,
|
|
21
|
+
);
|
|
22
|
+
const rangeTo = normalizeText(
|
|
23
|
+
range?.to ?? value.to ?? value.end ?? value.before,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if ('shortcut' in value || range || 'start' in value || 'end' in value || 'after' in value || 'before' in value) {
|
|
27
|
+
if (EMPTY_SHORTCUTS.has(shortcut) && !rangeFrom && !rangeTo) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if ((shortcut === 'custom' || !shortcut) && !rangeFrom && !rangeTo) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Object.values(value).some(isFilledFilterValue);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return normalizeText(value) !== '';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const countActiveFilters = (filters = {}) => {
|
|
42
|
+
if (!filters || typeof filters !== 'object' || Array.isArray(filters)) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Object.keys(filters).filter(key => {
|
|
47
|
+
if (['page', 'itemsPerPage', 'order', 'sort', 'pagination'].includes(key)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return isFilledFilterValue(filters[key]);
|
|
51
|
+
}).length;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const omitEmptyFilters = (filters = {}) => {
|
|
55
|
+
if (!filters || typeof filters !== 'object' || Array.isArray(filters)) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Object.keys(filters).reduce((accumulator, key) => {
|
|
60
|
+
if (isFilledFilterValue(filters[key])) {
|
|
61
|
+
accumulator[key] = filters[key];
|
|
62
|
+
}
|
|
63
|
+
return accumulator;
|
|
64
|
+
}, {});
|
|
65
|
+
};
|