@asteby/metacore-runtime-react 34.1.0 → 35.0.0

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.
@@ -22,6 +22,7 @@
22
22
  // value). A dedicated `?ids=` lookup is a follow-up; create flows — the common
23
23
  // case — start empty and never hit this.
24
24
  import { useEffect, useRef, useState } from 'react'
25
+ import { useTranslation } from 'react-i18next'
25
26
  import {
26
27
  Badge,
27
28
  Button,
@@ -205,6 +206,9 @@ export function DynamicSelectField({
205
206
  descriptionAsBadge = false,
206
207
  hideCreate = false,
207
208
  }: DynamicSelectFieldProps) {
209
+ const { t } = useTranslation()
210
+ const ph = (fallback: string) =>
211
+ field.placeholder ? t(field.placeholder, { defaultValue: field.placeholder }) : fallback
208
212
  const [open, setOpen] = useState(false)
209
213
  const [search, setSearch] = useState('')
210
214
  const [scanOpen, setScanOpen] = useState(false)
@@ -353,7 +357,7 @@ export function DynamicSelectField({
353
357
  <span className={'min-w-0 flex-1 truncate ' + (selectedOption ? '' : 'text-muted-foreground')}>
354
358
  {/* Never flash the raw id: until the eager fetch resolves the
355
359
  option, show a loading hint instead of String(value). */}
356
- {selectedOption?.label ?? (loading ? 'Cargando…' : field.placeholder || '—')}
360
+ {selectedOption?.label ?? (loading ? 'Cargando…' : ph('—'))}
357
361
  </span>
358
362
  </span>
359
363
  </Button>
@@ -386,7 +390,7 @@ export function DynamicSelectField({
386
390
  <span className={'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
387
391
  {blockedByDependency
388
392
  ? (dependsHint || DEFAULT_DEPENDS_HINT)
389
- : selectedLabel || field.placeholder || 'Buscar…'}
393
+ : selectedLabel || ph('Buscar…')}
390
394
  </span>
391
395
  {descriptionAsBadge && selectedOption?.description ? (
392
396
  <Badge variant="secondary" className="shrink-0 font-normal tabular-nums">
@@ -406,7 +410,7 @@ export function DynamicSelectField({
406
410
  >
407
411
  <Command shouldFilter={false}>
408
412
  <CommandInput
409
- placeholder={field.placeholder || 'Buscar…'}
413
+ placeholder={ph('Buscar…')}
410
414
  value={search}
411
415
  onValueChange={setSearch}
412
416
  />
@@ -33,10 +33,36 @@ export interface PrintDocumentArgs {
33
33
  * open → open the PDF in a new tab (user prints from the viewer).
34
34
  */
35
35
  mode?: 'print' | 'download' | 'open'
36
- /** Filename for the download mode (defaults to "<key>.pdf"). */
36
+ /**
37
+ * Hint filename for download mode. Prefer leaving this unset: the server
38
+ * expands `{{record.*}}` into Content-Disposition. A raw template string
39
+ * (e.g. "cfdi-{{record.number}}.pdf") must NOT be used as a.download —
40
+ * that is how downloads end up literally named with mustache braces.
41
+ */
37
42
  filename?: string
38
43
  }
39
44
 
45
+ /** Parse filename from Content-Disposition (RFC 5987 / quoted). */
46
+ export function filenameFromContentDisposition(header: string | undefined | null): string | undefined {
47
+ if (!header) return undefined
48
+ const star = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(header)
49
+ if (star?.[1]) {
50
+ try {
51
+ return decodeURIComponent(star[1].trim().replace(/^"|"$/g, ''))
52
+ } catch {
53
+ return star[1].trim().replace(/^"|"$/g, '')
54
+ }
55
+ }
56
+ const plain = /filename\s*=\s*"([^"]+)"|filename\s*=\s*([^;]+)/i.exec(header)
57
+ const raw = (plain?.[1] ?? plain?.[2] ?? '').trim()
58
+ return raw || undefined
59
+ }
60
+
61
+ /** True when a caller passed an unexpanded mustache template as filename. */
62
+ export function looksLikeFilenameTemplate(name: string | undefined): boolean {
63
+ return !!name && /\{\{/.test(name)
64
+ }
65
+
40
66
  /**
41
67
  * Returns a `printDocument(args)` callback. Resolves once the PDF has been
42
68
  * fetched and the browser action (print/download/open) has been kicked off;
@@ -65,9 +91,19 @@ export function usePrintDocument() {
65
91
  const cleanup = () => setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
66
92
 
67
93
  if (mode === 'download') {
94
+ const headers = (res as { headers?: Record<string, string> }).headers || {}
95
+ const fromHeader =
96
+ filenameFromContentDisposition(
97
+ headers['content-disposition'] || headers['Content-Disposition'],
98
+ ) || undefined
99
+ // Prefer server-expanded name; never use a raw {{record.*}} template.
100
+ const downloadName =
101
+ fromHeader ||
102
+ (!looksLikeFilenameTemplate(filename) ? filename : undefined) ||
103
+ `${key}.pdf`
68
104
  const a = document.createElement('a')
69
105
  a.href = blobUrl
70
- a.download = filename || `${key}.pdf`
106
+ a.download = downloadName
71
107
  document.body.appendChild(a)
72
108
  a.click()
73
109
  a.remove()