@object-ui/fields 5.1.1 → 5.2.1

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.
@@ -23,12 +23,19 @@ export interface CellRendererProps {
23
23
  */
24
24
  export declare function coerceToSafeValue(value: unknown): string | number | boolean | null | undefined;
25
25
  /**
26
- * Format currency value
26
+ * Format currency value. When `currency` is undefined, falls back to a
27
+ * plain number with thousands separators (no symbol). Silently assuming
28
+ * USD for unconfigured currency fields was the #1 source of "why is my
29
+ * RMB amount showing as dollars?" bug reports.
30
+ *
31
+ * Trailing `.00` is dropped when the value is a whole number — Salesforce
32
+ * convention: `$1,234.50` keeps cents; `$1,234` does not.
27
33
  */
28
34
  export declare function formatCurrency(value: number, currency?: string): string;
29
35
  /**
30
36
  * Format currency value in compact form for mobile display.
31
37
  * E.g., $150,000 → $150K, $1,200,000 → $1.2M
38
+ * When `currency` is undefined, returns a compact number without symbol.
32
39
  */
33
40
  export declare function formatCompactCurrency(value: number, currency?: string): string;
34
41
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/fields",
3
- "version": "5.1.1",
3
+ "version": "5.2.1",
4
4
  "description": "Field renderers and registry for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,12 +27,12 @@
27
27
  "clsx": "^2.1.1",
28
28
  "lucide-react": "^1.16.0",
29
29
  "tailwind-merge": "^3.6.0",
30
- "@object-ui/components": "5.1.1",
31
- "@object-ui/core": "5.1.1",
32
- "@object-ui/i18n": "5.1.1",
33
- "@object-ui/providers": "5.1.1",
34
- "@object-ui/react": "5.1.1",
35
- "@object-ui/types": "5.1.1"
30
+ "@object-ui/components": "5.2.1",
31
+ "@object-ui/core": "5.2.1",
32
+ "@object-ui/i18n": "5.2.1",
33
+ "@object-ui/providers": "5.2.1",
34
+ "@object-ui/react": "5.2.1",
35
+ "@object-ui/types": "5.2.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "react": "^18.0.0 || ^19.0.0",
package/src/index.tsx CHANGED
@@ -222,24 +222,49 @@ export function coerceToSafeValue(value: unknown): string | number | boolean | n
222
222
  }
223
223
 
224
224
  /**
225
- * Format currency value
225
+ * Format currency value. When `currency` is undefined, falls back to a
226
+ * plain number with thousands separators (no symbol). Silently assuming
227
+ * USD for unconfigured currency fields was the #1 source of "why is my
228
+ * RMB amount showing as dollars?" bug reports.
229
+ *
230
+ * Trailing `.00` is dropped when the value is a whole number — Salesforce
231
+ * convention: `$1,234.50` keeps cents; `$1,234` does not.
226
232
  */
227
- export function formatCurrency(value: number, currency: string = 'USD'): string {
233
+ export function formatCurrency(value: number, currency?: string): string {
234
+ const isWhole = Number.isFinite(value) && value === Math.trunc(value);
235
+ const maxFrac = isWhole ? 0 : 2;
236
+ if (!currency) {
237
+ return formatNumber(value, maxFrac);
238
+ }
228
239
  try {
229
240
  return new Intl.NumberFormat('en-US', {
230
241
  style: 'currency',
231
242
  currency,
243
+ minimumFractionDigits: 0,
244
+ maximumFractionDigits: maxFrac,
232
245
  }).format(value);
233
246
  } catch {
234
- return `${currency} ${value.toFixed(2)}`;
247
+ return `${currency} ${value.toFixed(maxFrac)}`;
235
248
  }
236
249
  }
237
250
 
238
251
  /**
239
252
  * Format currency value in compact form for mobile display.
240
253
  * E.g., $150,000 → $150K, $1,200,000 → $1.2M
254
+ * When `currency` is undefined, returns a compact number without symbol.
241
255
  */
242
- export function formatCompactCurrency(value: number, currency: string = 'USD'): string {
256
+ export function formatCompactCurrency(value: number, currency?: string): string {
257
+ if (!currency) {
258
+ try {
259
+ const formatted = new Intl.NumberFormat('en-US', {
260
+ notation: 'compact',
261
+ maximumFractionDigits: 1,
262
+ }).format(value);
263
+ return formatted.replace(/\.0(?=[KMBT])/, '');
264
+ } catch {
265
+ return String(value);
266
+ }
267
+ }
243
268
  try {
244
269
  const formatted = new Intl.NumberFormat('en-US', {
245
270
  style: 'currency',
@@ -340,9 +365,14 @@ export function formatDate(value: string | Date | number, style?: string): strin
340
365
  return formatRelativeDate(date);
341
366
  }
342
367
 
343
- // Default format: locale-aware human-readable (e.g. "Jan 15, 2024" or "2024/1/15")
368
+ // Default format: locale-aware human-readable. Drop the year when it
369
+ // matches the current year — Salesforce / HubSpot / Linear all do this
370
+ // because the year is rarely useful for in-progress records and the
371
+ // verbose "2026年7月21日" form crowds cards and table cells. Past- /
372
+ // future-year dates keep the year so users can disambiguate.
373
+ const isCurrentYear = date.getFullYear() === new Date().getFullYear();
344
374
  return date.toLocaleDateString(undefined, {
345
- year: 'numeric',
375
+ year: isCurrentYear ? undefined : 'numeric',
346
376
  month: 'short',
347
377
  day: 'numeric',
348
378
  });
@@ -399,24 +429,23 @@ export function CurrencyCellRenderer({ value, field }: CellRendererProps): React
399
429
 
400
430
  const safe = coerceToSafeValue(value);
401
431
  const currencyField = field as any;
402
- // Honor both `currency` (legacy/grid configs) and `defaultCurrency`
403
- // (the canonical key from `@objectstack/spec` Field.currency()). When
404
- // neither is supplied, fall back to USD so currency-typed fields always
405
- // render with a symbol — a bare "5,000,000.00" is indistinguishable from
406
- // a plain number and is the #1 source of "is this dollars or what?"
407
- // confusion in customer screenshots.
408
- const currency: string =
409
- currencyField.currency || currencyField.defaultCurrency || 'USD';
432
+ // Honor `currency` (legacy/grid configs), `defaultCurrency` (canonical
433
+ // top-level), and `currencyConfig.defaultCurrency` (the nested shape
434
+ // emitted by `@objectstack/spec` Field.currency() with currencyConfig).
435
+ // When none is supplied, render as a plain formatted number —
436
+ // silently assuming USD for unconfigured currency fields was
437
+ // misleading for non-USD orgs (e.g. RMB amounts displayed as $).
438
+ const currency: string | undefined =
439
+ currencyField.currency ||
440
+ currencyField.defaultCurrency ||
441
+ currencyField.currencyConfig?.defaultCurrency ||
442
+ undefined;
410
443
  const num = Number(safe);
411
- const decimals = currencyField.precision ?? currencyField.scale ?? currencyField.decimals ?? 2;
412
444
  const formatted = !isNaN(num)
413
445
  ? formatCurrency(num, currency)
414
446
  : String(safe);
415
- // Hide trailing fractional digits when the field opted into integer-only
416
- // precision (e.g. annual_revenue stored as whole units).
417
- const display = decimals === 0 ? formatted.replace(/[.,]00$/, '') : formatted;
418
447
 
419
- return <span className="tabular-nums font-medium whitespace-nowrap">{display}</span>;
448
+ return <span className="tabular-nums font-medium whitespace-nowrap">{formatted}</span>;
420
449
  }
421
450
 
422
451
  // Fields that store percentage values as whole numbers (0-100) rather than fractions (0-1)
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Textarea, EmptyValue } from '@object-ui/components';
3
+ import { useObjectTranslation } from '@object-ui/react';
3
4
  import { FieldWidgetProps } from './types';
4
5
 
5
6
  /**
@@ -8,6 +9,7 @@ import { FieldWidgetProps } from './types';
8
9
  * a rich text editor like TipTap, Lexical, or Slate.
9
10
  */
10
11
  export function RichTextField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
12
+ const { t } = useObjectTranslation();
11
13
  if (readonly) {
12
14
  return (
13
15
  <div
@@ -25,13 +27,18 @@ export function RichTextField({ value, onChange, field, readonly, errorMessage,
25
27
  return (
26
28
  <div className="space-y-2">
27
29
  <div className="flex items-center justify-between text-xs text-gray-500">
28
- <span>Format: {format}</span>
29
- <span className="italic">Rich text editor (basic)</span>
30
+ <span>{t('fields.richText.format', { format, defaultValue: `Format: ${format}` })}</span>
31
+ <span className="italic">
32
+ {t('fields.richText.basicEditorHint', { defaultValue: 'Rich text editor (basic)' })}
33
+ </span>
30
34
  </div>
31
35
  <Textarea
32
36
  value={value || ''}
33
37
  onChange={(e) => onChange(e.target.value)}
34
- placeholder={richField?.placeholder || 'Enter text...'}
38
+ placeholder={
39
+ richField?.placeholder ||
40
+ t('fields.richText.placeholder', { defaultValue: 'Enter text...' })
41
+ }
35
42
  disabled={readonly || props.disabled}
36
43
  rows={rows}
37
44
  className={`font-mono text-sm ${props.className || ''}`}