@controleonline/ui-default 1.0.272 → 1.0.273

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.
Files changed (41) hide show
  1. package/.github/workflows/publish-npm.yml +91 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/technical/DefaultTable.md +70 -1
  4. package/package.json +22 -22
  5. package/src/react/components/address/DefaultAddress.js +20 -9
  6. package/src/react/components/address/DefaultAddressCoordFields.js +59 -0
  7. package/src/react/components/address/LatLonReadonlyFields.js +2 -2
  8. package/src/react/components/address/defaultAddressStyles.js +13 -1
  9. package/src/react/components/files/DefaultFile.js +2 -2
  10. package/src/react/components/files/DefaultFileColumn.js +1 -1
  11. package/src/react/components/filters/DefaultColumnFilter.js +2 -7
  12. package/src/react/components/filters/DefaultExternalFilters.js +4 -11
  13. package/src/react/components/filters/filterValue.js +65 -0
  14. package/src/react/components/help/DefaultTooltip.js +73 -63
  15. package/src/react/components/help/DefaultTooltip.styles.js +23 -23
  16. package/src/react/components/inputs/DefaultInput.js +301 -210
  17. package/src/react/components/inputs/defaultInputUtils.js +193 -170
  18. package/src/react/components/table/DefaultColumnMenu.js +127 -109
  19. package/src/react/components/table/DefaultTable.js +4 -4
  20. package/src/react/components/table/DefaultTable.styles.js +10 -4
  21. package/src/react/components/table/DefaultTable.utils.js +38 -4
  22. package/src/react/components/table/DefaultTableRows.js +324 -153
  23. package/src/react/components/upload/DefaultUpload.js +6 -36
  24. package/src/react/components/upload/DefaultUpload.styles.js +5 -5
  25. package/src/react/components/upload/DefaultUploadAttachmentsList.js +11 -3
  26. package/src/react/hooks/usePostalCodeLookup.js +15 -1
  27. package/src/react/services/addressFormUtils.js +104 -38
  28. package/src/react/services/addressGeo.js +6 -3
  29. package/src/react/utils/tableVisibleColumnsPreferences.js +84 -3
  30. package/src/tests/react/components/files/DefaultFile.test.js +16 -0
  31. package/src/tests/react/components/files/DefaultFileColumn.company.test.js +24 -0
  32. package/src/tests/react/components/help/DefaultTooltip.test.js +93 -0
  33. package/src/tests/react/components/table/DefaultTable.utils.test.js +10 -0
  34. package/src/tests/react/components/table/DefaultTableRowActions.test.js +23 -0
  35. package/src/tests/react/components/table/DefaultTableRows.rowActions.test.js +152 -0
  36. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +31 -9
  37. package/src/tests/react/filters/filterValue.test.js +46 -0
  38. package/src/tests/react/hooks/usePostalCodeLookup.test.js +67 -0
  39. package/src/tests/react/inputs/defaultInputUtils.test.js +9 -1
  40. package/src/tests/react/services/addressFormUtils.test.js +6 -3
  41. 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.272",
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/conline-quasar-ui"
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.273",
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 === 'cep') {
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. Número/complemento/apelido são preservados.
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={formatCepMask(form.cep)}
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 when both coordinates are present and the user has not started
7
- * a manual edit; editable when coords are missing so the user can type them.
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?.defaultCompany?.id) {
8
- return peopleGetters.defaultCompany;
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?.defaultCompany?.id) return peopleGetters.defaultCompany;
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
- const isEmpty =
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
+ };