@open-mercato/ui 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3287.1.450f4ffb56
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +23 -14
- package/dist/backend/AppShell.js +8 -32
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +18 -0
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +149 -53
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/FilterBar.js +11 -14
- package/dist/backend/FilterBar.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +4 -6
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/filters/ActiveFilterChips.js +142 -0
- package/dist/backend/filters/ActiveFilterChips.js.map +7 -0
- package/dist/backend/filters/AdvancedFilterBuilder.js +689 -163
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +354 -0
- package/dist/backend/filters/AdvancedFilterPanel.js.map +7 -0
- package/dist/backend/filters/FilterEmptyState.js +41 -0
- package/dist/backend/filters/FilterEmptyState.js.map +7 -0
- package/dist/backend/filters/FilterFieldPicker.js +148 -0
- package/dist/backend/filters/FilterFieldPicker.js.map +7 -0
- package/dist/backend/filters/FilteredEmptyResults.js +26 -0
- package/dist/backend/filters/FilteredEmptyResults.js.map +7 -0
- package/dist/backend/filters/QuickFilters.js +41 -0
- package/dist/backend/filters/QuickFilters.js.map +7 -0
- package/dist/backend/filters/filterValidation.js +47 -0
- package/dist/backend/filters/filterValidation.js.map +7 -0
- package/dist/backend/filters/treeReducer.js +159 -0
- package/dist/backend/filters/treeReducer.js.map +7 -0
- package/dist/backend/hooks/useAdvancedFilter.js +82 -1
- package/dist/backend/hooks/useAdvancedFilter.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +11 -7
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/EventPatternInput.js +5 -2
- package/dist/backend/inputs/EventPatternInput.js.map +2 -2
- package/dist/backend/inputs/EventSelect.js +10 -4
- package/dist/backend/inputs/EventSelect.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +51 -47
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +196 -45
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +2 -2
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/utils/api.js +18 -1
- package/dist/backend/utils/api.js.map +2 -2
- package/dist/backend/utils/serverErrors.js +6 -2
- package/dist/backend/utils/serverErrors.js.map +2 -2
- package/dist/backend/utils/useAutoDiscoveredFields.js +1 -0
- package/dist/backend/utils/useAutoDiscoveredFields.js.map +2 -2
- package/dist/backend/utils/useCurrentUserId.js +31 -0
- package/dist/backend/utils/useCurrentUserId.js.map +7 -0
- package/dist/portal/utils/nav.js +17 -0
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/amount-input.js +128 -0
- package/dist/primitives/amount-input.js.map +7 -0
- package/dist/primitives/button-input.js +47 -0
- package/dist/primitives/button-input.js.map +7 -0
- package/dist/primitives/card-input.js +133 -0
- package/dist/primitives/card-input.js.map +7 -0
- package/dist/primitives/dialog.js +7 -5
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/email-input.js +29 -0
- package/dist/primitives/email-input.js.map +7 -0
- package/dist/primitives/password-input.js +64 -0
- package/dist/primitives/password-input.js.map +7 -0
- package/dist/primitives/search-input.js +57 -0
- package/dist/primitives/search-input.js.map +7 -0
- package/dist/primitives/select.js +43 -5
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/tag.js +4 -2
- package/dist/primitives/tag.js.map +2 -2
- package/dist/primitives/website-input.js +56 -0
- package/dist/primitives/website-input.js.map +7 -0
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +7 -30
- package/src/backend/CrudForm.tsx +17 -0
- package/src/backend/DataTable.tsx +307 -63
- package/src/backend/FilterBar.tsx +5 -6
- package/src/backend/__tests__/DataTable.extensions.test.tsx +170 -0
- package/src/backend/__tests__/serverErrors.test.ts +79 -1
- package/src/backend/columns/ColumnChooserPanel.tsx +4 -5
- package/src/backend/filters/ActiveFilterChips.tsx +179 -0
- package/src/backend/filters/AdvancedFilterBuilder.tsx +840 -237
- package/src/backend/filters/AdvancedFilterPanel.tsx +456 -0
- package/src/backend/filters/FilterEmptyState.tsx +51 -0
- package/src/backend/filters/FilterFieldPicker.tsx +139 -0
- package/src/backend/filters/FilteredEmptyResults.tsx +40 -0
- package/src/backend/filters/QuickFilters.tsx +62 -0
- package/src/backend/filters/__tests__/ActiveFilterChips.test.tsx +75 -0
- package/src/backend/filters/__tests__/AdvancedFilterBuilder.test.tsx +210 -0
- package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +212 -0
- package/src/backend/filters/__tests__/FilterEmptyState.test.tsx +31 -0
- package/src/backend/filters/__tests__/FilterFieldPicker.test.tsx +61 -0
- package/src/backend/filters/__tests__/FilteredEmptyResults.test.tsx +35 -0
- package/src/backend/filters/__tests__/QuickFilters.test.tsx +34 -0
- package/src/backend/filters/__tests__/filterValidation.test.ts +69 -0
- package/src/backend/filters/__tests__/treeReducer.test.ts +189 -0
- package/src/backend/filters/filterValidation.ts +61 -0
- package/src/backend/filters/treeReducer.ts +201 -0
- package/src/backend/hooks/__tests__/useAdvancedFilter.test.tsx +213 -0
- package/src/backend/hooks/useAdvancedFilter.ts +130 -0
- package/src/backend/inputs/ComboboxInput.tsx +32 -26
- package/src/backend/inputs/EventPatternInput.tsx +11 -2
- package/src/backend/inputs/EventSelect.tsx +13 -4
- package/src/backend/inputs/LookupSelect.tsx +75 -53
- package/src/backend/inputs/PhoneNumberField.tsx +237 -47
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +30 -0
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +4 -0
- package/src/backend/inputs/__tests__/PhoneNumberField.test.tsx +4 -0
- package/src/backend/messages/message-compose-form-groups.tsx +2 -2
- package/src/backend/utils/api.ts +27 -2
- package/src/backend/utils/serverErrors.ts +15 -8
- package/src/backend/utils/useAutoDiscoveredFields.ts +2 -0
- package/src/backend/utils/useCurrentUserId.ts +40 -0
- package/src/portal/utils/__tests__/nav.test.ts +80 -0
- package/src/portal/utils/nav.ts +23 -1
- package/src/primitives/__tests__/password-input.test.tsx +93 -0
- package/src/primitives/__tests__/search-input.test.tsx +95 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +26 -0
- package/src/primitives/amount-input.tsx +184 -0
- package/src/primitives/button-input.tsx +68 -0
- package/src/primitives/card-input.tsx +193 -0
- package/src/primitives/dialog.tsx +18 -7
- package/src/primitives/email-input.tsx +36 -0
- package/src/primitives/password-input.tsx +100 -0
- package/src/primitives/search-input.tsx +87 -0
- package/src/primitives/select.tsx +98 -11
- package/src/primitives/tag.tsx +3 -1
- package/src/primitives/website-input.tsx +72 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/PhoneNumberField.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { extractPhoneDigits, validatePhoneNumber } from '@open-mercato/shared/lib/phone'\nimport { Input } from '../../primitives/input'\n\nexport type PhoneDuplicateMatch = {\n id: string\n label: string\n href: string\n}\n\nexport type PhoneNumberFieldProps = {\n id?: string\n value?: string | null\n onValueChange: (next: string | undefined) => void\n onDigitsChange?: (digits: string | null) => void\n externalError?: string | null\n disabled?: boolean\n autoFocus?: boolean\n ariaLabel?: string\n ariaDescribedBy?: string\n placeholder?: string\n minDigits?: number\n checkingLabel?: string\n duplicateLabel?: (match: PhoneDuplicateMatch) => string\n duplicateLinkLabel?: string\n invalidLabel?: string\n onDuplicateLookup?: (normalizedValue: string) => Promise<PhoneDuplicateMatch | null>\n}\n\nconst DEFAULT_MIN_DIGITS = 6\nconst DEFAULT_INVALID_LABEL = 'Enter a valid phone number with country code (e.g. +1 212 555 1234)'\n\nexport function PhoneNumberField({\n id,\n value,\n onValueChange,\n onDigitsChange,\n externalError,\n disabled = false,\n autoFocus,\n ariaLabel,\n ariaDescribedBy,\n placeholder,\n minDigits = DEFAULT_MIN_DIGITS,\n checkingLabel,\n duplicateLabel,\n duplicateLinkLabel,\n invalidLabel,\n onDuplicateLookup,\n}: PhoneNumberFieldProps) {\n const [local, setLocal] = React.useState<string>(() => {\n if (value == null || value === '') return ''\n return String(value)\n })\n const [duplicate, setDuplicate] = React.useState<PhoneDuplicateMatch | null>(null)\n const [checking, setChecking] = React.useState(false)\n const [validationHint, setValidationHint] = React.useState<string | null>(null)\n const userEditingRef = React.useRef(false)\n const externalFieldError = externalError && externalError.trim().length > 0 ? externalError : null\n const errorMessage = externalFieldError ?? validationHint\n const errorId = errorMessage && id ? `${id}-error` : undefined\n const describedBy = [ariaDescribedBy, errorId].filter((part): part is string => Boolean(part)).join(' ') || undefined\n\n React.useEffect(() => {\n if (userEditingRef.current) return\n if (value == null || value === '') {\n setLocal('')\n onDigitsChange?.(null)\n return\n }\n const nextValue = String(value)\n setLocal(nextValue)\n onDigitsChange?.(extractPhoneDigits(nextValue) || null)\n }, [value, onDigitsChange])\n\n React.useEffect(() => {\n if (!onDuplicateLookup || disabled) {\n setDuplicate(null)\n setChecking(false)\n return\n }\n const digits = extractPhoneDigits(local)\n if (!digits || digits.length < minDigits) {\n setDuplicate(null)\n setChecking(false)\n return\n }\n\n let cancelled = false\n setChecking(true)\n const handle = window.setTimeout(async () => {\n try {\n const match = await onDuplicateLookup(digits)\n if (!cancelled) setDuplicate(match)\n } catch {\n if (!cancelled) setDuplicate(null)\n } finally {\n if (!cancelled) setChecking(false)\n }\n }, 350)\n\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [local, disabled, minDigits, onDuplicateLookup])\n\n const handleChange = React.useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const next = event.target.value\n const cleanDigits = extractPhoneDigits(next)\n userEditingRef.current = true\n setLocal(next)\n setValidationHint(null)\n onValueChange(next.length ? next : undefined)\n onDigitsChange?.(cleanDigits.length ? cleanDigits : null)\n },\n [onValueChange, onDigitsChange]\n )\n\n const handleBlur = React.useCallback(() => {\n userEditingRef.current = false\n const trimmed = local.trim()\n if (!trimmed) {\n setLocal('')\n setValidationHint(null)\n onValueChange(undefined)\n onDigitsChange?.(null)\n return\n }\n const result = validatePhoneNumber(trimmed)\n if (result.valid) {\n setLocal(result.normalized ?? '')\n setValidationHint(null)\n onValueChange(result.normalized || undefined)\n onDigitsChange?.(result.digits || null)\n } else {\n setLocal(trimmed)\n setValidationHint(invalidLabel ?? DEFAULT_INVALID_LABEL)\n onValueChange(trimmed)\n onDigitsChange?.(result.digits || null)\n }\n }, [invalidLabel, local, onDigitsChange, onValueChange])\n\n return (\n <div className=\"space-y-2\">\n <Input\n type=\"tel\"\n value={local}\n onChange={handleChange}\n onBlur={handleBlur}\n placeholder={placeholder}\n autoFocus={autoFocus}\n disabled={disabled}\n id={id}\n aria-label={ariaLabel}\n aria-describedby={describedBy}\n aria-invalid={errorMessage ? 'true' : undefined}\n data-crud-focus-target=\"\"\n />\n {errorMessage ? (\n <p id={errorId} className=\"text-xs text-destructive\">{errorMessage}</p>\n ) : null}\n {!disabled && duplicate && duplicateLabel && duplicateLinkLabel ? (\n <p className=\"text-xs text-amber-600\">\n {duplicateLabel(duplicate)}{' '}\n <a className=\"font-medium text-primary underline underline-offset-2\" href={duplicate.href}>\n {duplicateLinkLabel}\n </a>\n </p>\n ) : null}\n {!disabled && !duplicate && checking && checkingLabel ? (\n <p className=\"text-xs text-muted-foreground\">{checkingLabel}</p>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { extractPhoneDigits, validatePhoneNumber } from '@open-mercato/shared/lib/phone'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectItemLeading,\n SelectTrigger,\n} from '../../primitives/select'\n\nexport type PhoneDuplicateMatch = {\n id: string\n label: string\n href: string\n}\n\nexport type PhoneCountry = {\n /** ISO 3166-1 alpha-2 code. */\n iso2: string\n /** International dial code (with `+`). */\n dialCode: string\n /** Human-readable country name (English). Override per surface for i18n. */\n label: string\n /** Emoji flag (no asset dependency). */\n flag: string\n}\n\n/**\n * Static country list \u2014 focused on EU + Americas markets where Open Mercato\n * currently operates. Extend as needed; longest dial codes MUST appear before\n * their shorter ancestors (e.g. `+380` before `+38`) to keep prefix matching\n * deterministic. The default fallback is the first entry.\n */\nexport const PHONE_COUNTRIES: PhoneCountry[] = [\n { iso2: 'US', dialCode: '+1', label: 'United States', flag: '\uD83C\uDDFA\uD83C\uDDF8' },\n { iso2: 'CA', dialCode: '+1', label: 'Canada', flag: '\uD83C\uDDE8\uD83C\uDDE6' },\n { iso2: 'GB', dialCode: '+44', label: 'United Kingdom', flag: '\uD83C\uDDEC\uD83C\uDDE7' },\n { iso2: 'PL', dialCode: '+48', label: 'Poland', flag: '\uD83C\uDDF5\uD83C\uDDF1' },\n { iso2: 'DE', dialCode: '+49', label: 'Germany', flag: '\uD83C\uDDE9\uD83C\uDDEA' },\n { iso2: 'FR', dialCode: '+33', label: 'France', flag: '\uD83C\uDDEB\uD83C\uDDF7' },\n { iso2: 'ES', dialCode: '+34', label: 'Spain', flag: '\uD83C\uDDEA\uD83C\uDDF8' },\n { iso2: 'IT', dialCode: '+39', label: 'Italy', flag: '\uD83C\uDDEE\uD83C\uDDF9' },\n { iso2: 'NL', dialCode: '+31', label: 'Netherlands', flag: '\uD83C\uDDF3\uD83C\uDDF1' },\n { iso2: 'SE', dialCode: '+46', label: 'Sweden', flag: '\uD83C\uDDF8\uD83C\uDDEA' },\n { iso2: 'AT', dialCode: '+43', label: 'Austria', flag: '\uD83C\uDDE6\uD83C\uDDF9' },\n { iso2: 'CH', dialCode: '+41', label: 'Switzerland', flag: '\uD83C\uDDE8\uD83C\uDDED' },\n { iso2: 'PT', dialCode: '+351', label: 'Portugal', flag: '\uD83C\uDDF5\uD83C\uDDF9' },\n { iso2: 'CZ', dialCode: '+420', label: 'Czechia', flag: '\uD83C\uDDE8\uD83C\uDDFF' },\n { iso2: 'RO', dialCode: '+40', label: 'Romania', flag: '\uD83C\uDDF7\uD83C\uDDF4' },\n { iso2: 'UA', dialCode: '+380', label: 'Ukraine', flag: '\uD83C\uDDFA\uD83C\uDDE6' },\n]\n\nconst DEFAULT_COUNTRY = PHONE_COUNTRIES[0]\n// Match longer prefixes first to avoid e.g. `+1` shadowing `+1234` if any.\nconst COUNTRIES_BY_DIAL_LENGTH = [...PHONE_COUNTRIES].sort(\n (a, b) => b.dialCode.length - a.dialCode.length,\n)\n\nfunction findCountryByIso(iso2: string): PhoneCountry | undefined {\n return PHONE_COUNTRIES.find((c) => c.iso2 === iso2)\n}\n\nfunction parseCountryFromValue(value: string | null | undefined): PhoneCountry | null {\n if (!value) return null\n const trimmed = value.trim()\n if (!trimmed.startsWith('+')) return null\n for (const country of COUNTRIES_BY_DIAL_LENGTH) {\n if (trimmed.startsWith(country.dialCode)) return country\n }\n return null\n}\n\nfunction extractLocalNumber(value: string | null | undefined, dialCode: string): string {\n if (!value) return ''\n const trimmed = value.trim()\n if (!trimmed.startsWith(dialCode)) return trimmed\n return trimmed.slice(dialCode.length).trim()\n}\n\nfunction composeValue(country: PhoneCountry, localNumber: string): string {\n const trimmed = localNumber.trim()\n if (!trimmed) return ''\n if (trimmed.startsWith('+')) return trimmed\n return `${country.dialCode} ${trimmed}`\n}\n\nexport type PhoneNumberFieldProps = {\n id?: string\n value?: string | null\n onValueChange: (next: string | undefined) => void\n onDigitsChange?: (digits: string | null) => void\n externalError?: string | null\n disabled?: boolean\n autoFocus?: boolean\n ariaLabel?: string\n ariaDescribedBy?: string\n placeholder?: string\n minDigits?: number\n checkingLabel?: string\n duplicateLabel?: (match: PhoneDuplicateMatch) => string\n duplicateLinkLabel?: string\n invalidLabel?: string\n onDuplicateLookup?: (normalizedValue: string) => Promise<PhoneDuplicateMatch | null>\n /** Override the static country list (e.g. limit to specific markets). */\n countries?: PhoneCountry[]\n /** Initial country shown when `value` is empty / unparseable. Defaults to US. */\n defaultCountryIso2?: string\n}\n\nconst DEFAULT_MIN_DIGITS = 6\nconst DEFAULT_PLACEHOLDER = '(555) 000-0000'\n\nexport function PhoneNumberField({\n id,\n value,\n onValueChange,\n onDigitsChange,\n externalError,\n disabled = false,\n autoFocus,\n ariaLabel,\n ariaDescribedBy,\n placeholder,\n minDigits = DEFAULT_MIN_DIGITS,\n checkingLabel,\n duplicateLabel,\n duplicateLinkLabel,\n invalidLabel,\n onDuplicateLookup,\n countries: countriesProp,\n defaultCountryIso2,\n}: PhoneNumberFieldProps) {\n const t = useT()\n const resolvedInvalidLabel = invalidLabel ?? t(\n 'ui.inputs.phoneNumberField.invalid',\n 'Enter a valid phone number with country code (e.g. +1 212 555 1234)'\n )\n const resolvedCheckingLabel = checkingLabel ?? t(\n 'ui.inputs.phoneNumberField.checking',\n 'Checking for duplicates\u2026'\n )\n const resolvedDuplicateLinkLabel = duplicateLinkLabel ?? t(\n 'ui.inputs.phoneNumberField.duplicateLink',\n 'Open record'\n )\n const resolvedPlaceholder = placeholder ?? DEFAULT_PLACEHOLDER\n const countries = countriesProp ?? PHONE_COUNTRIES\n const fallbackCountry = React.useMemo(\n () => (defaultCountryIso2 && findCountryByIso(defaultCountryIso2)) || DEFAULT_COUNTRY,\n [defaultCountryIso2],\n )\n\n const initialCountry = React.useMemo(\n () => parseCountryFromValue(value) ?? fallbackCountry,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n )\n const [country, setCountry] = React.useState<PhoneCountry>(initialCountry)\n const [localNumber, setLocalNumber] = React.useState<string>(() =>\n extractLocalNumber(value, initialCountry.dialCode),\n )\n const [duplicate, setDuplicate] = React.useState<PhoneDuplicateMatch | null>(null)\n const [checking, setChecking] = React.useState(false)\n const [validationHint, setValidationHint] = React.useState<string | null>(null)\n const [focused, setFocused] = React.useState(false)\n const userEditingRef = React.useRef(false)\n\n const externalFieldError = externalError && externalError.trim().length > 0 ? externalError : null\n const errorMessage = externalFieldError ?? validationHint\n const errorId = errorMessage && id ? `${id}-error` : undefined\n const describedBy = [ariaDescribedBy, errorId].filter((part): part is string => Boolean(part)).join(' ') || undefined\n const composedValue = composeValue(country, localNumber)\n\n // Sync local state when `value` updates externally and the user is not editing.\n React.useEffect(() => {\n if (userEditingRef.current) return\n if (value == null || value === '') {\n setLocalNumber('')\n onDigitsChange?.(null)\n return\n }\n const parsed = parseCountryFromValue(value)\n if (parsed) setCountry(parsed)\n const next = extractLocalNumber(value, parsed?.dialCode ?? country.dialCode)\n setLocalNumber(next)\n onDigitsChange?.(extractPhoneDigits(String(value)) || null)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value])\n\n // Debounced duplicate lookup.\n React.useEffect(() => {\n if (!onDuplicateLookup || disabled) {\n setDuplicate(null)\n setChecking(false)\n return\n }\n const digits = extractPhoneDigits(composedValue)\n if (!digits || digits.length < minDigits) {\n setDuplicate(null)\n setChecking(false)\n return\n }\n\n let cancelled = false\n setChecking(true)\n const handle = window.setTimeout(async () => {\n try {\n const match = await onDuplicateLookup(digits)\n if (!cancelled) setDuplicate(match)\n } catch {\n if (!cancelled) setDuplicate(null)\n } finally {\n if (!cancelled) setChecking(false)\n }\n }, 350)\n\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [composedValue, disabled, minDigits, onDuplicateLookup])\n\n const emit = React.useCallback(\n (nextCountry: PhoneCountry, nextLocal: string) => {\n const composed = composeValue(nextCountry, nextLocal)\n onValueChange(composed.length ? composed : undefined)\n onDigitsChange?.(extractPhoneDigits(composed) || null)\n },\n [onDigitsChange, onValueChange],\n )\n\n const handleCountryChange = React.useCallback(\n (iso2: string) => {\n const next = findCountryByIso(iso2)\n if (!next) return\n userEditingRef.current = true\n setCountry(next)\n setValidationHint(null)\n emit(next, localNumber)\n },\n [emit, localNumber],\n )\n\n const handleLocalChange = React.useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const next = event.target.value\n userEditingRef.current = true\n setLocalNumber(next)\n setValidationHint(null)\n emit(country, next)\n },\n [country, emit],\n )\n\n const handleBlur = React.useCallback(() => {\n userEditingRef.current = false\n setFocused(false)\n const trimmed = localNumber.trim()\n if (!trimmed) {\n setLocalNumber('')\n setValidationHint(null)\n onValueChange(undefined)\n onDigitsChange?.(null)\n return\n }\n const composed = composeValue(country, trimmed)\n const result = validatePhoneNumber(composed)\n if (result.valid) {\n const normalizedLocal = extractLocalNumber(result.normalized ?? composed, country.dialCode)\n setLocalNumber(normalizedLocal)\n setValidationHint(null)\n onValueChange(result.normalized || undefined)\n onDigitsChange?.(result.digits || null)\n } else {\n setValidationHint(resolvedInvalidLabel)\n onValueChange(composed)\n onDigitsChange?.(result.digits || null)\n }\n }, [country, localNumber, onDigitsChange, onValueChange, resolvedInvalidLabel])\n\n const containerErrorBorder = errorMessage ? 'border-status-error-icon' : 'border-input'\n const containerFocusBorder = errorMessage\n ? 'border-status-error-icon shadow-focus'\n : 'border-brand-violet shadow-focus'\n\n return (\n <div className=\"space-y-2\">\n <div\n className={cn(\n 'flex items-stretch w-full rounded-md border bg-background shadow-xs transition-colors',\n disabled\n ? 'bg-bg-disabled border-border-disabled cursor-not-allowed'\n : focused\n ? containerFocusBorder\n : `${containerErrorBorder} hover:border-foreground/30`,\n )}\n aria-invalid={errorMessage ? 'true' : undefined}\n >\n <Select value={country.iso2} onValueChange={handleCountryChange} disabled={disabled}>\n <SelectTrigger\n aria-label={ariaLabel ? `${ariaLabel} country` : 'Country code'}\n className={cn(\n 'h-auto w-auto shrink-0 gap-1.5 rounded-none rounded-l-md border-0 bg-transparent px-2.5 py-2 shadow-none',\n 'hover:bg-muted/40 focus:bg-muted/40 focus-visible:shadow-none focus-visible:border-0',\n 'disabled:bg-transparent disabled:hover:bg-transparent',\n )}\n onFocus={() => setFocused(true)}\n onBlur={() => setFocused(false)}\n >\n <span className=\"text-base leading-none\" aria-hidden=\"true\">{country.flag}</span>\n <span className=\"text-sm text-foreground tabular-nums\">{country.dialCode}</span>\n </SelectTrigger>\n <SelectContent align=\"start\">\n {countries.map((c) => (\n <SelectItem key={`${c.iso2}-${c.dialCode}`} value={c.iso2}>\n <SelectItemLeading>\n <span className=\"text-base leading-none\">{c.flag}</span>\n </SelectItemLeading>\n <span className=\"flex-1 truncate\">{c.label}</span>\n <span className=\"ml-2 text-xs text-muted-foreground tabular-nums\">{c.dialCode}</span>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <div aria-hidden=\"true\" className=\"w-px self-stretch bg-input\" />\n <input\n type=\"tel\"\n inputMode=\"tel\"\n autoComplete=\"tel-national\"\n value={localNumber}\n onChange={handleLocalChange}\n onBlur={handleBlur}\n onFocus={() => setFocused(true)}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n disabled={disabled}\n id={id}\n aria-label={ariaLabel}\n aria-describedby={describedBy}\n aria-invalid={errorMessage ? 'true' : undefined}\n data-crud-focus-target=\"\"\n className={cn(\n 'flex-1 min-w-0 bg-transparent px-3 py-2 text-sm leading-5 outline-none',\n 'placeholder:text-muted-foreground',\n 'disabled:cursor-not-allowed disabled:text-muted-foreground',\n )}\n />\n </div>\n {errorMessage ? (\n <p id={errorId} role=\"alert\" className=\"text-xs text-status-error-text\">{errorMessage}</p>\n ) : null}\n {!disabled && duplicate && duplicateLabel ? (\n <p className=\"text-xs text-status-warning-text\">\n {duplicateLabel(duplicate)}{' '}\n <a className=\"font-medium text-brand-violet underline underline-offset-2\" href={duplicate.href}>\n {resolvedDuplicateLinkLabel}\n </a>\n </p>\n ) : null}\n {!disabled && !duplicate && checking ? (\n <p className=\"text-xs text-muted-foreground\">{resolvedCheckingLabel}</p>\n ) : null}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/SwitchableMarkdownInput.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport type { PluggableList } from 'unified'\nimport { LoadingMessage } from '../detail/LoadingMessage'\nimport { useMarkdownRemarkPlugins } from '../markdown/useMarkdownRemarkPlugins'\nimport { useTheme } from '../../theme'\n\nexport type SwitchableMarkdownInputProps = {\n value: string\n onChange: (value: string) => void\n isMarkdownEnabled: boolean\n disableMarkdown?: boolean\n height?: number\n placeholder?: string\n rows?: number\n textareaRef?: React.Ref<HTMLTextAreaElement>\n onTextareaInput?: React.FormEventHandler<HTMLTextAreaElement>\n textareaClassName?: string\n editorWrapperClassName?: string\n editorClassName?: string\n disabled?: boolean\n remarkPlugins?: PluggableList\n}\n\ntype UiMarkdownEditorProps = {\n value?: string\n height?: number\n onChange?: (value?: string) => void\n previewOptions?: { remarkPlugins?: unknown[] }\n}\n\nconst isTestEnv =\n typeof process !== 'undefined' &&\n (process.env.NODE_ENV === 'test' || typeof process.env.JEST_WORKER_ID !== 'undefined')\n\nconst MarkdownEditorTestStub: React.ComponentType<UiMarkdownEditorProps> = ({ value, onChange }) => (\n <textarea\n className=\"min-h-[160px] w-full rounded border px-3 py-2 text-sm\"\n value={value ?? ''}\n onChange={(event) => onChange?.(event.target.value)}\n />\n)\n\nconst UiMarkdownEditor = isTestEnv\n ? MarkdownEditorTestStub\n : (dynamic(() => import('@uiw/react-md-editor'), {\n ssr: false,\n loading: () => (\n <LoadingMessage\n label=\"Loading editor...\"\n className=\"min-h-[220px] justify-center\"\n />\n ),\n }) as unknown as React.ComponentType<UiMarkdownEditorProps>)\n\nexport function SwitchableMarkdownInput({\n value,\n onChange,\n isMarkdownEnabled,\n disableMarkdown,\n height = 220,\n placeholder,\n rows = 3,\n textareaRef,\n onTextareaInput,\n textareaClassName,\n editorWrapperClassName,\n editorClassName,\n disabled,\n remarkPlugins,\n}: SwitchableMarkdownInputProps) {\n const resolvedPlugins = useMarkdownRemarkPlugins(remarkPlugins)\n const { resolvedTheme } = useTheme()\n const editorWrapperClasses =\n editorWrapperClassName ?? 'w-full rounded-lg border border-muted-foreground/20 bg-background p-2'\n const editorClasses = editorClassName ?? 'w-full'\n const textareaClasses =\n textareaClassName\n ?? 'w-full resize-none overflow-hidden rounded-lg border border-muted-foreground/20 bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\n if (isMarkdownEnabled && !disableMarkdown) {\n return (\n <div className={editorWrapperClasses}>\n <div data-color-mode={resolvedTheme} className={editorClasses}>\n <UiMarkdownEditor\n value={value}\n height={height}\n onChange={(nextValue) => onChange(typeof nextValue === 'string' ? nextValue : '')}\n previewOptions={resolvedPlugins.length ? { remarkPlugins: resolvedPlugins } : undefined}\n />\n </div>\n </div>\n )\n }\n\n return (\n <textarea\n ref={textareaRef}\n rows={rows}\n className={textareaClasses}\n placeholder={placeholder}\n value={value}\n onChange={(event) => onChange(event.target.value)}\n onInput={onTextareaInput}\n disabled={disabled}\n />\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["/**\n * @deprecated Prefer the DS `RichEditor` primitive from\n * `@open-mercato/ui/primitives/rich-editor` for any new rich-text input.\n *\n * `RichEditor` produces sanitized HTML (with the DS toolbar, color palette,\n * Figma-aligned styling, and auto-overflow toolbar variants). The DS direction\n * is to consolidate on a single rich-text format (HTML) so that user-authored\n * content renders consistently across email, exports, and the customer portal.\n *\n * `SwitchableMarkdownInput` is kept as a backward-compatibility shim for\n * existing Markdown-backed surfaces (notably `customers` Notes). It will be\n * removed once those surfaces migrate their storage format from Markdown to\n * sanitized HTML \u2014 track the migration in the spec referenced from\n * `customers/AGENTS.md`.\n *\n * Migration:\n *\n * ```diff\n * - import { SwitchableMarkdownInput } from '@open-mercato/ui/backend/inputs/SwitchableMarkdownInput'\n * - <SwitchableMarkdownInput value={md} onChange={setMd} isMarkdownEnabled={enabled} />\n * + import { RichEditor } from '@open-mercato/ui/primitives/rich-editor'\n * + <RichEditor value={html} onChange={setHtml} variant=\"basic\" />\n * ```\n *\n * For surfaces that genuinely need plain Markdown storage (e.g. agent prompts,\n * code-adjacent docs), keep using `SwitchableMarkdownInput` until a Markdown\n * mode lands on `RichEditor` \u2014 but do NOT introduce new Markdown surfaces.\n */\n\"use client\"\n\nimport * as React from 'react'\nimport dynamic from 'next/dynamic'\nimport type { PluggableList } from 'unified'\nimport { LoadingMessage } from '../detail/LoadingMessage'\nimport { useMarkdownRemarkPlugins } from '../markdown/useMarkdownRemarkPlugins'\nimport { useTheme } from '../../theme'\n\n/** @deprecated See module-level JSDoc. */\nexport type SwitchableMarkdownInputProps = {\n value: string\n onChange: (value: string) => void\n isMarkdownEnabled: boolean\n disableMarkdown?: boolean\n height?: number\n placeholder?: string\n rows?: number\n textareaRef?: React.Ref<HTMLTextAreaElement>\n onTextareaInput?: React.FormEventHandler<HTMLTextAreaElement>\n textareaClassName?: string\n editorWrapperClassName?: string\n editorClassName?: string\n disabled?: boolean\n remarkPlugins?: PluggableList\n}\n\ntype UiMarkdownEditorProps = {\n value?: string\n height?: number\n onChange?: (value?: string) => void\n previewOptions?: { remarkPlugins?: unknown[] }\n}\n\nconst isTestEnv =\n typeof process !== 'undefined' &&\n (process.env.NODE_ENV === 'test' || typeof process.env.JEST_WORKER_ID !== 'undefined')\n\nconst MarkdownEditorTestStub: React.ComponentType<UiMarkdownEditorProps> = ({ value, onChange }) => (\n <textarea\n className=\"min-h-[160px] w-full rounded border px-3 py-2 text-sm\"\n value={value ?? ''}\n onChange={(event) => onChange?.(event.target.value)}\n />\n)\n\nconst UiMarkdownEditor = isTestEnv\n ? MarkdownEditorTestStub\n : (dynamic(() => import('@uiw/react-md-editor'), {\n ssr: false,\n loading: () => (\n <LoadingMessage\n label=\"Loading editor...\"\n className=\"min-h-[220px] justify-center\"\n />\n ),\n }) as unknown as React.ComponentType<UiMarkdownEditorProps>)\n\n/** @deprecated Use `RichEditor` from `@open-mercato/ui/primitives/rich-editor` for new code. See module JSDoc. */\nexport function SwitchableMarkdownInput({\n value,\n onChange,\n isMarkdownEnabled,\n disableMarkdown,\n height = 220,\n placeholder,\n rows = 3,\n textareaRef,\n onTextareaInput,\n textareaClassName,\n editorWrapperClassName,\n editorClassName,\n disabled,\n remarkPlugins,\n}: SwitchableMarkdownInputProps) {\n const resolvedPlugins = useMarkdownRemarkPlugins(remarkPlugins)\n const { resolvedTheme } = useTheme()\n const editorWrapperClasses =\n editorWrapperClassName ?? 'w-full rounded-lg border border-muted-foreground/20 bg-background p-2'\n const editorClasses = editorClassName ?? 'w-full'\n const textareaClasses =\n textareaClassName\n ?? 'w-full resize-none overflow-hidden rounded-lg border border-muted-foreground/20 bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\n if (isMarkdownEnabled && !disableMarkdown) {\n return (\n <div className={editorWrapperClasses}>\n <div data-color-mode={resolvedTheme} className={editorClasses}>\n <UiMarkdownEditor\n value={value}\n height={height}\n onChange={(nextValue) => onChange(typeof nextValue === 'string' ? nextValue : '')}\n previewOptions={resolvedPlugins.length ? { remarkPlugins: resolvedPlugins } : undefined}\n />\n </div>\n </div>\n )\n }\n\n return (\n <textarea\n ref={textareaRef}\n rows={rows}\n className={textareaClasses}\n placeholder={placeholder}\n value={value}\n onChange={(event) => onChange(event.target.value)}\n onInput={onTextareaInput}\n disabled={disabled}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAmEE;AApCF,OAAO,aAAa;AAEpB,SAAS,sBAAsB;AAC/B,SAAS,gCAAgC;AACzC,SAAS,gBAAgB;AA2BzB,MAAM,YACJ,OAAO,YAAY,gBAClB,QAAQ,IAAI,aAAa,UAAU,OAAO,QAAQ,IAAI,mBAAmB;AAE5E,MAAM,yBAAqE,CAAC,EAAE,OAAO,SAAS,MAC5F;AAAA,EAAC;AAAA;AAAA,IACC,WAAU;AAAA,IACV,OAAO,SAAS;AAAA,IAChB,UAAU,CAAC,UAAU,WAAW,MAAM,OAAO,KAAK;AAAA;AACpD;AAGF,MAAM,mBAAmB,YACrB,yBACC,QAAQ,MAAM,OAAO,sBAAsB,GAAG;AAAA,EAC7C,KAAK;AAAA,EACL,SAAS,MACP;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,WAAU;AAAA;AAAA,EACZ;AAEJ,CAAC;AAGE,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,kBAAkB,yBAAyB,aAAa;AAC9D,QAAM,EAAE,cAAc,IAAI,SAAS;AACnC,QAAM,uBACJ,0BAA0B;AAC5B,QAAM,gBAAgB,mBAAmB;AACzC,QAAM,kBACJ,qBACG;AAEL,MAAI,qBAAqB,CAAC,iBAAiB;AACzC,WACE,oBAAC,SAAI,WAAW,sBACd,8BAAC,SAAI,mBAAiB,eAAe,WAAW,eAC9C;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,UAAU,CAAC,cAAc,SAAS,OAAO,cAAc,WAAW,YAAY,EAAE;AAAA,QAChF,gBAAgB,gBAAgB,SAAS,EAAE,eAAe,gBAAgB,IAAI;AAAA;AAAA,IAChF,GACF,GACF;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,WAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,MAChD,SAAS;AAAA,MACT;AAAA;AAAA,EACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { FileCode, Globe, Lock } from "lucide-react";
|
|
3
3
|
import { IconButton } from "../../primitives/icon-button.js";
|
|
4
4
|
import { Input } from "../../primitives/input.js";
|
|
5
|
+
import { EmailInput } from "../../primitives/email-input.js";
|
|
5
6
|
import { Label } from "../../primitives/label.js";
|
|
6
7
|
import {
|
|
7
8
|
Select,
|
|
@@ -159,10 +160,9 @@ function ComposeModeFields({ compose }) {
|
|
|
159
160
|
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: compose.visibility === "public" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
160
161
|
/* @__PURE__ */ jsx(Label, { htmlFor: "messages-compose-external-email", children: compose.t("messages.externalEmail", "External email") }),
|
|
161
162
|
/* @__PURE__ */ jsx(
|
|
162
|
-
|
|
163
|
+
EmailInput,
|
|
163
164
|
{
|
|
164
165
|
id: "messages-compose-external-email",
|
|
165
|
-
type: "email",
|
|
166
166
|
value: compose.externalEmail,
|
|
167
167
|
onChange: (event) => compose.setExternalEmail(event.target.value),
|
|
168
168
|
placeholder: compose.t("messages.placeholders.externalEmail", "name@example.com")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/messages/message-compose-form-groups.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\nimport { FileCode, Globe, Lock } from 'lucide-react'\nimport { type CrudField } from '../CrudForm'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Input } from '../../primitives/input'\nimport { Label } from '../../primitives/label'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\nimport { Switch } from '../../primitives/switch'\nimport { AttachmentsSection } from '../detail/AttachmentsSection'\nimport { SwitchableMarkdownInput } from '../inputs/SwitchableMarkdownInput'\nimport { TagsInput } from '../inputs/TagsInput'\nimport { MessagePrioritySelector } from './MessagePrioritySelector'\nimport type { UseMessageComposeResult } from './useMessageCompose'\n\ntype ComposeProps = {\n compose: UseMessageComposeResult\n}\n\nfunction RecipientTagsInput({ compose }: ComposeProps) {\n return (\n <TagsInput\n value={compose.recipientIds}\n onChange={compose.setRecipientIds}\n selectedOptions={compose.selectedRecipientOptions}\n resolveLabel={compose.resolveRecipientLabel}\n loadSuggestions={compose.loadRecipientSuggestions}\n placeholder={compose.t('messages.placeholders.recipients', 'Search recipients...')}\n allowCustomValues={false}\n showSuggestionsOnFocus={false}\n />\n )\n}\n\nfunction VisibilitySelector({ compose }: ComposeProps) {\n return (\n <>\n <Label>{compose.t('messages.visibility', 'Visibility')}</Label>\n <div\n className=\"inline-flex items-center gap-1 rounded-md border bg-background p-1\"\n role=\"radiogroup\"\n aria-label={compose.t('messages.visibility', 'Visibility')}\n >\n <IconButton\n type=\"button\"\n size=\"xs\"\n variant={compose.visibility === 'internal' ? 'outline' : 'ghost'}\n role=\"radio\"\n aria-checked={compose.visibility === 'internal'}\n aria-label={compose.t('messages.visibilityInternal', 'Internal')}\n title={compose.t('messages.visibilityInternal', 'Internal')}\n className=\"h-7 w-7\"\n onClick={() => compose.setVisibility('internal')}\n >\n <Lock className=\"h-3.5 w-3.5\" />\n </IconButton>\n <IconButton\n type=\"button\"\n size=\"xs\"\n variant={compose.visibility === 'public' ? 'outline' : 'ghost'}\n role=\"radio\"\n aria-checked={compose.visibility === 'public'}\n aria-label={compose.t('messages.visibilityPublic', 'Public')}\n title={compose.t('messages.visibilityPublic', 'Public')}\n className=\"h-7 w-7\"\n onClick={() => compose.setVisibility('public')}\n >\n <Globe className=\"h-3.5 w-3.5\" />\n </IconButton>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {compose.visibility === 'public'\n ? compose.t('messages.visibilityPublicHint', 'Public messages are sent to external email only.')\n : compose.t('messages.visibilityInternalHint', 'Internal messages are sent to selected system users.')}\n </p>\n </>\n )\n}\n\nfunction ContextActionsSection({ compose }: ComposeProps) {\n if (!compose.shouldShowContextActions) return null\n\n return (\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {compose.normalizedRequiredActionMode === 'optional' ? (\n <div className=\"flex items-center justify-between rounded border px-3 py-2 sm:col-span-2\">\n <div>\n <p className=\"text-sm font-medium\">\n {compose.t('messages.composer.objectPicker.actionRequiredLabel', 'Action required')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {compose.t('messages.composer.objectPicker.actionRequiredHint', 'Mark this object as requiring recipient action.')}\n </p>\n </div>\n <Switch checked={compose.contextActionRequired} onCheckedChange={compose.setContextActionRequired} />\n </div>\n ) : null}\n {compose.normalizedRequiredActionMode === 'required' || compose.contextActionRequired ? (\n <div className=\"space-y-2 sm:col-span-2\">\n <Label htmlFor=\"messages-compose-context-action-type\">\n {compose.t('messages.composer.objectPicker.actionTypeLabel', 'Action type')}\n </Label>\n <Select\n value={compose.contextActionType || undefined}\n onValueChange={(value) => compose.setContextActionType(value || '')}\n >\n <SelectTrigger id=\"messages-compose-context-action-type\">\n <SelectValue placeholder={compose.t('messages.composer.objectPicker.actionTypePlaceholder', 'Select action')} />\n </SelectTrigger>\n <SelectContent>\n {compose.contextActionOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction PrioritySelector({ compose }: ComposeProps) {\n return (\n <>\n <Label>{compose.t('messages.priority', 'Priority')}</Label>\n <MessagePrioritySelector\n value={compose.priority}\n onChange={compose.setPriority}\n t={compose.t}\n />\n </>\n )\n}\n\nfunction MarkdownBodySection({\n compose,\n label,\n placeholder,\n inputId,\n rows,\n textareaClassName,\n}: ComposeProps & {\n label: string\n placeholder: string\n inputId: string\n rows: number\n textareaClassName: string\n}) {\n return (\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between gap-2\">\n <Label htmlFor={inputId}>{label}</Label>\n <IconButton\n type=\"button\"\n size=\"sm\"\n variant={compose.bodyFormat === 'markdown' ? 'outline' : 'ghost'}\n aria-pressed={compose.bodyFormat === 'markdown'}\n onClick={() => compose.setBodyFormat((previousValue) => (previousValue === 'markdown' ? 'text' : 'markdown'))}\n title={compose.t('messages.bodyFormat.toggle', 'Toggle markdown')}\n >\n <FileCode className=\"h-4 w-4\" />\n </IconButton>\n </div>\n <div id={inputId}>\n <SwitchableMarkdownInput\n value={compose.body}\n onChange={compose.setBody}\n isMarkdownEnabled={compose.bodyFormat === 'markdown'}\n rows={rows}\n placeholder={placeholder}\n textareaClassName={textareaClassName}\n />\n </div>\n </div>\n )\n}\n\nfunction ComposeModeFields({ compose }: ComposeProps) {\n return (\n <>\n <div className=\"space-y-3\">\n <div className=\"grid gap-3 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n {compose.visibility === 'public' ? (\n <>\n <Label htmlFor=\"messages-compose-external-email\">{compose.t('messages.externalEmail', 'External email')}</Label>\n <Input\n id=\"messages-compose-external-email\"\n type=\"email\"\n value={compose.externalEmail}\n onChange={(event) => compose.setExternalEmail(event.target.value)}\n placeholder={compose.t('messages.placeholders.externalEmail', 'name@example.com')}\n />\n </>\n ) : (\n <>\n <Label htmlFor=\"messages-compose-recipients\">{compose.t('messages.to', 'To')}</Label>\n <RecipientTagsInput compose={compose} />\n </>\n )}\n </div>\n\n <div className=\"space-y-2\">\n <VisibilitySelector compose={compose} />\n </div>\n </div>\n\n <ContextActionsSection compose={compose} />\n </div>\n\n <div className=\"grid gap-3 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"messages-compose-subject\">{compose.t('messages.subject', 'Subject')}</Label>\n <Input\n id=\"messages-compose-subject\"\n value={compose.subject}\n onChange={(event) => compose.setSubject(event.target.value)}\n placeholder={compose.t('messages.placeholders.subject', 'Enter subject...')}\n />\n </div>\n\n <div className=\"space-y-2\">\n <PrioritySelector compose={compose} />\n </div>\n </div>\n\n <MarkdownBodySection\n compose={compose}\n label={compose.t('messages.body', 'Message')}\n placeholder={compose.t('messages.placeholders.body', 'Write your message...')}\n inputId=\"messages-compose-body\"\n rows={8}\n textareaClassName=\"min-h-[180px] w-full rounded-md border bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n />\n\n <div className=\"space-y-2\">\n <Label>{compose.t('messages.attachedFiles', 'Attachments')}</Label>\n <AttachmentsSection\n entityId={compose.attachmentEntityId}\n recordId={compose.attachmentRecordId}\n showHeader={false}\n compact\n onChanged={() => {\n void compose.loadAttachmentIds().catch(() => null)\n }}\n />\n </div>\n </>\n )\n}\n\nfunction ReplyModeFields({ compose }: ComposeProps) {\n return (\n <>\n <MarkdownBodySection\n compose={compose}\n label={compose.t('messages.replyBody', 'Reply')}\n placeholder={compose.t('messages.placeholders.replyBody', 'Write your reply...')}\n inputId=\"messages-compose-body\"\n rows={8}\n textareaClassName=\"min-h-[180px] w-full rounded-md border bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n />\n\n <div className=\"space-y-2\">\n <Label>{compose.t('messages.attachedFiles', 'Attachments')}</Label>\n <AttachmentsSection\n entityId={compose.attachmentEntityId}\n recordId={compose.attachmentRecordId}\n showHeader={false}\n compact\n onChanged={() => {\n void compose.loadAttachmentIds().catch(() => null)\n }}\n />\n </div>\n </>\n )\n}\n\nfunction ForwardModeFields({ compose }: ComposeProps) {\n return (\n <>\n <div className=\"space-y-2\">\n <Label htmlFor=\"messages-compose-recipients\">{compose.t('messages.to', 'To')}</Label>\n <RecipientTagsInput compose={compose} />\n </div>\n\n <MarkdownBodySection\n compose={compose}\n label={compose.t('messages.forwardContent', 'Forwarded content')}\n placeholder={compose.t('messages.placeholders.forwardContent', 'Review and edit forwarded content...')}\n inputId=\"messages-forward-note\"\n rows={6}\n textareaClassName=\"min-h-[140px] w-full rounded-md border bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n />\n </>\n )\n}\n\nfunction EmailDeliverySection({ compose }: ComposeProps) {\n if (compose.variant === 'reply' || compose.variant === 'forward') {\n return null\n }\n\n if (compose.isComposePublicVisibility) {\n return (\n <div className=\"rounded border px-3 py-2\">\n <p className=\"text-sm font-medium\">{compose.t('messages.sendViaEmail', 'Also send via email')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.sendViaEmailForcedPublic', 'For public visibility, email delivery is always enabled.')}</p>\n </div>\n )\n }\n\n return (\n <div className=\"flex items-center justify-between rounded border px-3 py-2\">\n <div>\n <p className=\"text-sm font-medium\">{compose.t('messages.sendViaEmail', 'Also send via email')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.sendViaEmailHint', 'Recipients will receive an email copy with a secure link.')}</p>\n </div>\n <Switch checked={compose.sendViaEmail} onCheckedChange={compose.setSendViaEmail} />\n </div>\n )\n}\n\nfunction ReplyForwardOptionsRow({ compose }: ComposeProps) {\n if (compose.variant !== 'reply' && compose.variant !== 'forward') {\n return null\n }\n\n return (\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {compose.variant === 'forward' ? (\n <div className=\"flex items-center justify-between rounded border px-3 py-2\">\n <div>\n <p className=\"text-sm font-medium\">{compose.t('messages.includeAttachments', 'Include attachments')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.includeAttachmentsHint', 'Carry over attachments from the original message.')}</p>\n </div>\n <Switch checked={compose.includeAttachments} onCheckedChange={compose.setIncludeAttachments} />\n </div>\n ) : (\n <div className=\"flex items-center justify-between rounded border px-3 py-2\">\n <div>\n <p className=\"text-sm font-medium\">{compose.t('messages.replyAll', 'Reply all')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.replyAllHint', 'Include all original recipients.')}</p>\n </div>\n <Switch checked={compose.replyAll} onCheckedChange={compose.setReplyAll} />\n </div>\n )}\n <div className=\"flex items-center justify-between rounded border px-3 py-2\">\n <div>\n <p className=\"text-sm font-medium\">{compose.t('messages.sendViaEmail', 'Also send via email')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.sendViaEmailHint', 'Recipients will receive an email copy with a secure link.')}</p>\n </div>\n <Switch checked={compose.sendViaEmail} onCheckedChange={compose.setSendViaEmail} />\n </div>\n </div>\n )\n}\n\nfunction MessageComposeFormBody({ compose }: ComposeProps) {\n return (\n <div className=\"space-y-4\" onKeyDown={compose.handleKeyDown}>\n {compose.contextPreview ? (\n <div className=\"rounded border bg-muted/30 p-3 text-sm\">\n {compose.contextPreview}\n </div>\n ) : null}\n {compose.variant === 'compose' ? <ComposeModeFields compose={compose} /> : null}\n {compose.variant === 'reply' ? <ReplyModeFields compose={compose} /> : null}\n {compose.variant === 'forward' ? <ForwardModeFields compose={compose} /> : null}\n <ReplyForwardOptionsRow compose={compose} />\n <EmailDeliverySection compose={compose} />\n {compose.submitError ? <p className=\"text-sm text-destructive\">{compose.submitError}</p> : null}\n </div>\n )\n}\n\nexport function createMessageComposeFormGroups(compose: UseMessageComposeResult): CrudField[] {\n return [{\n id: 'composer',\n label: '',\n type: 'custom',\n component: () => <MessageComposeFormBody compose={compose} />,\n }]\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport { FileCode, Globe, Lock } from 'lucide-react'\nimport { type CrudField } from '../CrudForm'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Input } from '../../primitives/input'\nimport { EmailInput } from '../../primitives/email-input'\nimport { Label } from '../../primitives/label'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\nimport { Switch } from '../../primitives/switch'\nimport { AttachmentsSection } from '../detail/AttachmentsSection'\nimport { SwitchableMarkdownInput } from '../inputs/SwitchableMarkdownInput'\nimport { TagsInput } from '../inputs/TagsInput'\nimport { MessagePrioritySelector } from './MessagePrioritySelector'\nimport type { UseMessageComposeResult } from './useMessageCompose'\n\ntype ComposeProps = {\n compose: UseMessageComposeResult\n}\n\nfunction RecipientTagsInput({ compose }: ComposeProps) {\n return (\n <TagsInput\n value={compose.recipientIds}\n onChange={compose.setRecipientIds}\n selectedOptions={compose.selectedRecipientOptions}\n resolveLabel={compose.resolveRecipientLabel}\n loadSuggestions={compose.loadRecipientSuggestions}\n placeholder={compose.t('messages.placeholders.recipients', 'Search recipients...')}\n allowCustomValues={false}\n showSuggestionsOnFocus={false}\n />\n )\n}\n\nfunction VisibilitySelector({ compose }: ComposeProps) {\n return (\n <>\n <Label>{compose.t('messages.visibility', 'Visibility')}</Label>\n <div\n className=\"inline-flex items-center gap-1 rounded-md border bg-background p-1\"\n role=\"radiogroup\"\n aria-label={compose.t('messages.visibility', 'Visibility')}\n >\n <IconButton\n type=\"button\"\n size=\"xs\"\n variant={compose.visibility === 'internal' ? 'outline' : 'ghost'}\n role=\"radio\"\n aria-checked={compose.visibility === 'internal'}\n aria-label={compose.t('messages.visibilityInternal', 'Internal')}\n title={compose.t('messages.visibilityInternal', 'Internal')}\n className=\"h-7 w-7\"\n onClick={() => compose.setVisibility('internal')}\n >\n <Lock className=\"h-3.5 w-3.5\" />\n </IconButton>\n <IconButton\n type=\"button\"\n size=\"xs\"\n variant={compose.visibility === 'public' ? 'outline' : 'ghost'}\n role=\"radio\"\n aria-checked={compose.visibility === 'public'}\n aria-label={compose.t('messages.visibilityPublic', 'Public')}\n title={compose.t('messages.visibilityPublic', 'Public')}\n className=\"h-7 w-7\"\n onClick={() => compose.setVisibility('public')}\n >\n <Globe className=\"h-3.5 w-3.5\" />\n </IconButton>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {compose.visibility === 'public'\n ? compose.t('messages.visibilityPublicHint', 'Public messages are sent to external email only.')\n : compose.t('messages.visibilityInternalHint', 'Internal messages are sent to selected system users.')}\n </p>\n </>\n )\n}\n\nfunction ContextActionsSection({ compose }: ComposeProps) {\n if (!compose.shouldShowContextActions) return null\n\n return (\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {compose.normalizedRequiredActionMode === 'optional' ? (\n <div className=\"flex items-center justify-between rounded border px-3 py-2 sm:col-span-2\">\n <div>\n <p className=\"text-sm font-medium\">\n {compose.t('messages.composer.objectPicker.actionRequiredLabel', 'Action required')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {compose.t('messages.composer.objectPicker.actionRequiredHint', 'Mark this object as requiring recipient action.')}\n </p>\n </div>\n <Switch checked={compose.contextActionRequired} onCheckedChange={compose.setContextActionRequired} />\n </div>\n ) : null}\n {compose.normalizedRequiredActionMode === 'required' || compose.contextActionRequired ? (\n <div className=\"space-y-2 sm:col-span-2\">\n <Label htmlFor=\"messages-compose-context-action-type\">\n {compose.t('messages.composer.objectPicker.actionTypeLabel', 'Action type')}\n </Label>\n <Select\n value={compose.contextActionType || undefined}\n onValueChange={(value) => compose.setContextActionType(value || '')}\n >\n <SelectTrigger id=\"messages-compose-context-action-type\">\n <SelectValue placeholder={compose.t('messages.composer.objectPicker.actionTypePlaceholder', 'Select action')} />\n </SelectTrigger>\n <SelectContent>\n {compose.contextActionOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction PrioritySelector({ compose }: ComposeProps) {\n return (\n <>\n <Label>{compose.t('messages.priority', 'Priority')}</Label>\n <MessagePrioritySelector\n value={compose.priority}\n onChange={compose.setPriority}\n t={compose.t}\n />\n </>\n )\n}\n\nfunction MarkdownBodySection({\n compose,\n label,\n placeholder,\n inputId,\n rows,\n textareaClassName,\n}: ComposeProps & {\n label: string\n placeholder: string\n inputId: string\n rows: number\n textareaClassName: string\n}) {\n return (\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between gap-2\">\n <Label htmlFor={inputId}>{label}</Label>\n <IconButton\n type=\"button\"\n size=\"sm\"\n variant={compose.bodyFormat === 'markdown' ? 'outline' : 'ghost'}\n aria-pressed={compose.bodyFormat === 'markdown'}\n onClick={() => compose.setBodyFormat((previousValue) => (previousValue === 'markdown' ? 'text' : 'markdown'))}\n title={compose.t('messages.bodyFormat.toggle', 'Toggle markdown')}\n >\n <FileCode className=\"h-4 w-4\" />\n </IconButton>\n </div>\n <div id={inputId}>\n <SwitchableMarkdownInput\n value={compose.body}\n onChange={compose.setBody}\n isMarkdownEnabled={compose.bodyFormat === 'markdown'}\n rows={rows}\n placeholder={placeholder}\n textareaClassName={textareaClassName}\n />\n </div>\n </div>\n )\n}\n\nfunction ComposeModeFields({ compose }: ComposeProps) {\n return (\n <>\n <div className=\"space-y-3\">\n <div className=\"grid gap-3 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n {compose.visibility === 'public' ? (\n <>\n <Label htmlFor=\"messages-compose-external-email\">{compose.t('messages.externalEmail', 'External email')}</Label>\n <EmailInput\n id=\"messages-compose-external-email\"\n value={compose.externalEmail}\n onChange={(event) => compose.setExternalEmail(event.target.value)}\n placeholder={compose.t('messages.placeholders.externalEmail', 'name@example.com')}\n />\n </>\n ) : (\n <>\n <Label htmlFor=\"messages-compose-recipients\">{compose.t('messages.to', 'To')}</Label>\n <RecipientTagsInput compose={compose} />\n </>\n )}\n </div>\n\n <div className=\"space-y-2\">\n <VisibilitySelector compose={compose} />\n </div>\n </div>\n\n <ContextActionsSection compose={compose} />\n </div>\n\n <div className=\"grid gap-3 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"messages-compose-subject\">{compose.t('messages.subject', 'Subject')}</Label>\n <Input\n id=\"messages-compose-subject\"\n value={compose.subject}\n onChange={(event) => compose.setSubject(event.target.value)}\n placeholder={compose.t('messages.placeholders.subject', 'Enter subject...')}\n />\n </div>\n\n <div className=\"space-y-2\">\n <PrioritySelector compose={compose} />\n </div>\n </div>\n\n <MarkdownBodySection\n compose={compose}\n label={compose.t('messages.body', 'Message')}\n placeholder={compose.t('messages.placeholders.body', 'Write your message...')}\n inputId=\"messages-compose-body\"\n rows={8}\n textareaClassName=\"min-h-[180px] w-full rounded-md border bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n />\n\n <div className=\"space-y-2\">\n <Label>{compose.t('messages.attachedFiles', 'Attachments')}</Label>\n <AttachmentsSection\n entityId={compose.attachmentEntityId}\n recordId={compose.attachmentRecordId}\n showHeader={false}\n compact\n onChanged={() => {\n void compose.loadAttachmentIds().catch(() => null)\n }}\n />\n </div>\n </>\n )\n}\n\nfunction ReplyModeFields({ compose }: ComposeProps) {\n return (\n <>\n <MarkdownBodySection\n compose={compose}\n label={compose.t('messages.replyBody', 'Reply')}\n placeholder={compose.t('messages.placeholders.replyBody', 'Write your reply...')}\n inputId=\"messages-compose-body\"\n rows={8}\n textareaClassName=\"min-h-[180px] w-full rounded-md border bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n />\n\n <div className=\"space-y-2\">\n <Label>{compose.t('messages.attachedFiles', 'Attachments')}</Label>\n <AttachmentsSection\n entityId={compose.attachmentEntityId}\n recordId={compose.attachmentRecordId}\n showHeader={false}\n compact\n onChanged={() => {\n void compose.loadAttachmentIds().catch(() => null)\n }}\n />\n </div>\n </>\n )\n}\n\nfunction ForwardModeFields({ compose }: ComposeProps) {\n return (\n <>\n <div className=\"space-y-2\">\n <Label htmlFor=\"messages-compose-recipients\">{compose.t('messages.to', 'To')}</Label>\n <RecipientTagsInput compose={compose} />\n </div>\n\n <MarkdownBodySection\n compose={compose}\n label={compose.t('messages.forwardContent', 'Forwarded content')}\n placeholder={compose.t('messages.placeholders.forwardContent', 'Review and edit forwarded content...')}\n inputId=\"messages-forward-note\"\n rows={6}\n textareaClassName=\"min-h-[140px] w-full rounded-md border bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n />\n </>\n )\n}\n\nfunction EmailDeliverySection({ compose }: ComposeProps) {\n if (compose.variant === 'reply' || compose.variant === 'forward') {\n return null\n }\n\n if (compose.isComposePublicVisibility) {\n return (\n <div className=\"rounded border px-3 py-2\">\n <p className=\"text-sm font-medium\">{compose.t('messages.sendViaEmail', 'Also send via email')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.sendViaEmailForcedPublic', 'For public visibility, email delivery is always enabled.')}</p>\n </div>\n )\n }\n\n return (\n <div className=\"flex items-center justify-between rounded border px-3 py-2\">\n <div>\n <p className=\"text-sm font-medium\">{compose.t('messages.sendViaEmail', 'Also send via email')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.sendViaEmailHint', 'Recipients will receive an email copy with a secure link.')}</p>\n </div>\n <Switch checked={compose.sendViaEmail} onCheckedChange={compose.setSendViaEmail} />\n </div>\n )\n}\n\nfunction ReplyForwardOptionsRow({ compose }: ComposeProps) {\n if (compose.variant !== 'reply' && compose.variant !== 'forward') {\n return null\n }\n\n return (\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {compose.variant === 'forward' ? (\n <div className=\"flex items-center justify-between rounded border px-3 py-2\">\n <div>\n <p className=\"text-sm font-medium\">{compose.t('messages.includeAttachments', 'Include attachments')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.includeAttachmentsHint', 'Carry over attachments from the original message.')}</p>\n </div>\n <Switch checked={compose.includeAttachments} onCheckedChange={compose.setIncludeAttachments} />\n </div>\n ) : (\n <div className=\"flex items-center justify-between rounded border px-3 py-2\">\n <div>\n <p className=\"text-sm font-medium\">{compose.t('messages.replyAll', 'Reply all')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.replyAllHint', 'Include all original recipients.')}</p>\n </div>\n <Switch checked={compose.replyAll} onCheckedChange={compose.setReplyAll} />\n </div>\n )}\n <div className=\"flex items-center justify-between rounded border px-3 py-2\">\n <div>\n <p className=\"text-sm font-medium\">{compose.t('messages.sendViaEmail', 'Also send via email')}</p>\n <p className=\"text-xs text-muted-foreground\">{compose.t('messages.sendViaEmailHint', 'Recipients will receive an email copy with a secure link.')}</p>\n </div>\n <Switch checked={compose.sendViaEmail} onCheckedChange={compose.setSendViaEmail} />\n </div>\n </div>\n )\n}\n\nfunction MessageComposeFormBody({ compose }: ComposeProps) {\n return (\n <div className=\"space-y-4\" onKeyDown={compose.handleKeyDown}>\n {compose.contextPreview ? (\n <div className=\"rounded border bg-muted/30 p-3 text-sm\">\n {compose.contextPreview}\n </div>\n ) : null}\n {compose.variant === 'compose' ? <ComposeModeFields compose={compose} /> : null}\n {compose.variant === 'reply' ? <ReplyModeFields compose={compose} /> : null}\n {compose.variant === 'forward' ? <ForwardModeFields compose={compose} /> : null}\n <ReplyForwardOptionsRow compose={compose} />\n <EmailDeliverySection compose={compose} />\n {compose.submitError ? <p className=\"text-sm text-destructive\">{compose.submitError}</p> : null}\n </div>\n )\n}\n\nexport function createMessageComposeFormGroups(compose: UseMessageComposeResult): CrudField[] {\n return [{\n id: 'composer',\n label: '',\n type: 'custom',\n component: () => <MessageComposeFormBody compose={compose} />,\n }]\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -17,6 +17,22 @@ function mergeHeaders(base, scoped) {
|
|
|
17
17
|
function readRedirectOverride(headers, headerName) {
|
|
18
18
|
return headers.get(headerName) === "0";
|
|
19
19
|
}
|
|
20
|
+
function isSameOriginRequest(input) {
|
|
21
|
+
if (typeof window === "undefined") return false;
|
|
22
|
+
const host = window.location?.host;
|
|
23
|
+
if (!host) return false;
|
|
24
|
+
let urlString;
|
|
25
|
+
if (typeof input === "string") urlString = input;
|
|
26
|
+
else if (input instanceof URL) urlString = input.toString();
|
|
27
|
+
else if (typeof Request !== "undefined" && input instanceof Request) urlString = input.url;
|
|
28
|
+
else return false;
|
|
29
|
+
if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true;
|
|
30
|
+
try {
|
|
31
|
+
return new URL(urlString).host === host;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
async function withScopedApiHeaders(headers, run) {
|
|
21
37
|
return scopedHeaders.withScopedHeaders(headers, run);
|
|
22
38
|
}
|
|
@@ -83,7 +99,8 @@ async function apiFetch(input, init) {
|
|
|
83
99
|
);
|
|
84
100
|
}
|
|
85
101
|
const scoped = scopedHeaders.resolveScopedHeaders();
|
|
86
|
-
const
|
|
102
|
+
const baseInit = Object.keys(scoped).length ? { ...init ?? {}, headers: mergeHeaders(init?.headers, scoped) } : init ?? {};
|
|
103
|
+
const mergedInit = baseInit.credentials ? baseInit : isSameOriginRequest(input) ? { ...baseInit, credentials: "include" } : baseInit;
|
|
87
104
|
const requestHeaders = new Headers(mergedInit?.headers);
|
|
88
105
|
const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, "x-om-unauthorized-redirect");
|
|
89
106
|
const disableForbiddenRedirect = readRedirectOverride(requestHeaders, "x-om-forbidden-redirect");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/api.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nexport function redirectToForbiddenLogin(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n if (window.location.pathname.startsWith('/login')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n const current = window.location.pathname + window.location.search\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const fallbackRoles = DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n const effectiveRoles = roles.length ? roles : fallbackRoles\n const query = features.length\n ? `requireFeature=${encodeURIComponent(features.join(','))}`\n : effectiveRoles.length\n ? `requireRole=${encodeURIComponent(effectiveRoles.map(String).join(','))}`\n : ''\n const url = query\n ? `/login?${query}&redirect=${encodeURIComponent(current)}`\n : `/login?redirect=${encodeURIComponent(current)}`\n flash('Insufficient permissions. Redirecting to login\u2026', 'warning')\n setTimeout(() => { window.location.href = url }, 60)\n } catch {\n // no-op\n }\n}\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const mergedInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n const res = await baseFetch(input, mergedInit)\n const pathname = typeof window !== 'undefined' ? window.location.pathname : ''\n const onLoginPage = pathname.startsWith('/login')\n const onPortalRoute = /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n // eslint-disable-next-line no-console\n console.warn('[apiFetch] Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
|
|
5
|
-
"mappings": ";AAGA,SAAS,aAAa;AACtB,SAAS,oCAAoC;AAC7C,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,+BAA+B;AACxC,SAAS,+BAA+B;AAExC,MAAM,gBAAgB,wBAAwB;AAE9C,SAAS,aAAa,MAA+B,QAAyC;AAC5F,QAAM,UAAU,IAAI,QAAQ,QAAQ,CAAC,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,QAAQ,IAAI,GAAG,EAAG;AACtB,YAAQ,IAAI,KAAK,KAAK;AAAA,EACxB;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,SAAkB,YAA6B;AAC3E,SAAO,QAAQ,IAAI,UAAU,MAAM;AACrC;AAEA,eAAsB,qBAAwB,SAAiC,KAAmC;AAChH,SAAO,cAAc,kBAAkB,SAAS,GAAG;AACrD;AAEO,MAAM,0BAA0B,MAAM;AAAA,EAE3C,YAAY,UAAU,gBAAgB;AACpC,UAAM,OAAO;AAFf,SAAS,SAAS;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,2BAA2B;AACzC,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,UAAU,OAAO,SAAS,WAAW,OAAO,SAAS;AAE3D,MAAI,OAAO,SAAS,SAAS,WAAW,WAAW,EAAG;AAEtD,MAAI,wBAAwB,KAAK,OAAO,SAAS,QAAQ,EAAG;AAC5D,MAAI;AACF,UAAM,iDAA4C,SAAS;AAC3D,eAAW,MAAM;AACf,aAAO,SAAS,OAAO,sCAAsC,mBAAmB,OAAO,CAAC;AAAA,IAC1F,GAAG,EAAE;AAAA,EACP,QAAQ;AAAA,EAER;AACF;AAEO,MAAM,uBAAuB,MAAM;AAAA,EAExC,YAAY,UAAU,aAAa;AACjC,UAAM,OAAO;AAFf,SAAS,SAAS;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,0BAAoC,CAAC,OAAO;AAEzC,SAAS,sBAAsB,KAAoD;AACxF,MAAI,KAAK,yBAAyB,IAAI,sBAAsB,QAAQ;AAClE,8BAA0B,CAAC,GAAG,IAAI,qBAAqB,EAAE,IAAI,MAAM;AAAA,EACrE;AACF;AAEO,SAAS,yBAAyB,SAAmF;AAC1H,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI,OAAO,SAAS,SAAS,WAAW,QAAQ,EAAG;AAEnD,MAAI,wBAAwB,KAAK,OAAO,SAAS,QAAQ,EAAG;AAC5D,MAAI;AACF,UAAM,UAAU,OAAO,SAAS,WAAW,OAAO,SAAS;AAC3D,UAAM,WAAW,SAAS,kBAAkB,OAAO,OAAO,KAAK,CAAC;AAChE,UAAM,QAAQ,SAAS,eAAe,OAAO,OAAO,KAAK,CAAC;AAC1D,UAAM,gBAAgB,wBAAwB,OAAO,OAAO;AAC5D,UAAM,iBAAiB,MAAM,SAAS,QAAQ;AAC9C,UAAM,QAAQ,SAAS,SACnB,kBAAkB,mBAAmB,SAAS,KAAK,GAAG,CAAC,CAAC,KACxD,eAAe,SACb,eAAe,mBAAmB,eAAe,IAAI,MAAM,EAAE,KAAK,GAAG,CAAC,CAAC,KACvE;AACN,UAAM,MAAM,QACR,UAAU,KAAK,aAAa,mBAAmB,OAAO,CAAC,KACvD,mBAAmB,mBAAmB,OAAO,CAAC;AAClD,UAAM,wDAAmD,SAAS;AAClE,eAAW,MAAM;AAAE,aAAO,SAAS,OAAO;AAAA,IAAI,GAAG,EAAE;AAAA,EACrD,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,SAAS,OAA0B,MAAuC;AAE9F,QAAM,gBACJ,OAAO,WAAW,cACb,OAAsD,oBACvD;AACN,QAAM,gBAAiB,WAAyD;AAChF,QAAM,YAAY,iBAAiB;AACnC,MAAI,CAAC,WAAW;AACd,WAAO,IAAI;AAAA,MACT,KAAK,UAAU,EAAE,OAAO,6CAA6C,CAAC;AAAA,MACtE,EAAE,QAAQ,KAAK,SAAS,EAAE,gBAAgB,mBAAmB,EAAE;AAAA,IACjE;AAAA,EACF;AACA,QAAM,SAAS,cAAc,qBAAqB;AAClD,QAAM,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n// Simple fetch wrapper that redirects to session refresh on 401 (Unauthorized)\n// Used across UI data utilities to avoid duplication.\nimport { flash } from '../FlashMessages'\nimport { deserializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { pushOperation } from '../operations/store'\nimport { pushPartialIndexWarning } from '../indexes/store'\nimport { createScopedHeaderStack } from './scopedHeaderStack'\n\nconst scopedHeaders = createScopedHeaderStack()\n\nfunction mergeHeaders(base: HeadersInit | undefined, scoped: Record<string, string>): Headers {\n const headers = new Headers(base ?? {})\n for (const [key, value] of Object.entries(scoped)) {\n if (headers.has(key)) continue\n headers.set(key, value)\n }\n return headers\n}\n\nfunction readRedirectOverride(headers: Headers, headerName: string): boolean {\n return headers.get(headerName) === '0'\n}\n\nfunction isSameOriginRequest(input: RequestInfo | URL): boolean {\n if (typeof window === 'undefined') return false\n const host = window.location?.host\n if (!host) return false\n let urlString: string\n if (typeof input === 'string') urlString = input\n else if (input instanceof URL) urlString = input.toString()\n else if (typeof Request !== 'undefined' && input instanceof Request) urlString = input.url\n else return false\n if (!/^[a-z][a-z0-9+.-]*:/i.test(urlString)) return true\n try {\n return new URL(urlString).host === host\n } catch {\n return false\n }\n}\n\nexport async function withScopedApiHeaders<T>(headers: Record<string, string>, run: () => Promise<T>): Promise<T> {\n return scopedHeaders.withScopedHeaders(headers, run)\n}\n\nexport class UnauthorizedError extends Error {\n readonly status = 401\n constructor(message = 'Unauthorized') {\n super(message)\n this.name = 'UnauthorizedError'\n }\n}\n\nexport function redirectToSessionRefresh() {\n if (typeof window === 'undefined') return\n const current = window.location.pathname + window.location.search\n // Avoid redirect loops if already on an auth/session route\n if (window.location.pathname.startsWith('/api/auth')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n flash('Session expired. Redirecting to sign in\u2026', 'warning')\n setTimeout(() => {\n window.location.href = `/api/auth/session/refresh?redirect=${encodeURIComponent(current)}`\n }, 20)\n } catch {\n // no-op\n }\n}\n\nexport class ForbiddenError extends Error {\n readonly status = 403\n constructor(message = 'Forbidden') {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\nlet DEFAULT_FORBIDDEN_ROLES: string[] = ['admin']\n\nexport function setAuthRedirectConfig(cfg: { defaultForbiddenRoles?: readonly string[] }) {\n if (cfg?.defaultForbiddenRoles && cfg.defaultForbiddenRoles.length) {\n DEFAULT_FORBIDDEN_ROLES = [...cfg.defaultForbiddenRoles].map(String)\n }\n}\n\nexport function redirectToForbiddenLogin(options?: { requiredRoles?: string[] | null; requiredFeatures?: string[] | null }) {\n if (typeof window === 'undefined') return\n if (window.location.pathname.startsWith('/login')) return\n // Portal routes have their own customer auth \u2014 never redirect to staff login\n if (/\\/[^/]+\\/portal(\\/|$)/.test(window.location.pathname)) return\n try {\n const current = window.location.pathname + window.location.search\n const features = options?.requiredFeatures?.filter(Boolean) ?? []\n const roles = options?.requiredRoles?.filter(Boolean) ?? []\n const fallbackRoles = DEFAULT_FORBIDDEN_ROLES.filter(Boolean)\n const effectiveRoles = roles.length ? roles : fallbackRoles\n const query = features.length\n ? `requireFeature=${encodeURIComponent(features.join(','))}`\n : effectiveRoles.length\n ? `requireRole=${encodeURIComponent(effectiveRoles.map(String).join(','))}`\n : ''\n const url = query\n ? `/login?${query}&redirect=${encodeURIComponent(current)}`\n : `/login?redirect=${encodeURIComponent(current)}`\n flash('Insufficient permissions. Redirecting to login\u2026', 'warning')\n setTimeout(() => { window.location.href = url }, 60)\n } catch {\n // no-op\n }\n}\n\nexport async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n type FetchType = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n const originalFetch =\n typeof window !== 'undefined'\n ? (window as Window & { __omOriginalFetch?: FetchType }).__omOriginalFetch\n : undefined\n const fallbackFetch = (globalThis as typeof globalThis & { fetch?: FetchType }).fetch\n const baseFetch = originalFetch ?? fallbackFetch\n if (!baseFetch) {\n return new Response(\n JSON.stringify({ error: 'Fetch API is not available in this runtime' }),\n { status: 503, headers: { 'content-type': 'application/json' } },\n )\n }\n const scoped = scopedHeaders.resolveScopedHeaders()\n const baseInit: RequestInit = Object.keys(scoped).length\n ? { ...(init ?? {}), headers: mergeHeaders(init?.headers, scoped) }\n : init ?? {}\n // Only auto-inject credentials: 'include' for same-origin requests so cookies\n // round-trip across Next.js proxy.ts rewrites (custom-domain portal flows)\n // without leaking session cookies to third-party hosts.\n const mergedInit: RequestInit = baseInit.credentials\n ? baseInit\n : isSameOriginRequest(input)\n ? { ...baseInit, credentials: 'include' }\n : baseInit\n const requestHeaders = new Headers(mergedInit?.headers)\n const disableUnauthorizedRedirect = readRedirectOverride(requestHeaders, 'x-om-unauthorized-redirect')\n const disableForbiddenRedirect = readRedirectOverride(requestHeaders, 'x-om-forbidden-redirect')\n const res = await baseFetch(input, mergedInit)\n const pathname = typeof window !== 'undefined' ? window.location.pathname : ''\n const onLoginPage = pathname.startsWith('/login')\n const onPortalRoute = /\\/[^/]+\\/portal(\\/|$)/.test(pathname)\n if (res.status === 401) {\n // Trigger same redirect flow as protected pages\n // Skip for staff login page and all portal routes (portal has its own auth)\n if (!onLoginPage && !onPortalRoute && !disableUnauthorizedRedirect) {\n redirectToSessionRefresh()\n // Throw a typed error for callers that might still handle it\n throw new UnauthorizedError(await res.text().catch(() => 'Unauthorized'))\n }\n return res\n }\n if (res.status === 403) {\n // Try to read requiredRoles from JSON body; ignore if not JSON\n let roles: string[] | null = null\n let features: string[] | null = null\n let payload: unknown = null\n const aclData = await readJsonSafe<Record<string, unknown>>(res.clone(), null)\n if (aclData && typeof aclData === 'object') {\n if (Array.isArray(aclData.requiredRoles)) {\n roles = aclData.requiredRoles.map((r) => String(r))\n }\n if (Array.isArray(aclData.requiredFeatures)) {\n features = aclData.requiredFeatures.map((f) => String(f))\n }\n payload = aclData\n }\n // Only redirect if not already on login page or a portal route\n if (!onLoginPage && !onPortalRoute && !disableForbiddenRedirect) {\n const target =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : (typeof Request !== 'undefined' && input instanceof Request)\n ? input.url\n : 'unknown'\n try {\n // eslint-disable-next-line no-console\n console.warn('[apiFetch] Forbidden response', {\n url: target,\n status: res.status,\n requiredRoles: roles,\n requiredFeatures: features,\n details: payload,\n })\n } catch {}\n const hasAclHints = Boolean((roles && roles.length) || (features && features.length))\n if (hasAclHints) {\n redirectToForbiddenLogin({ requiredRoles: roles, requiredFeatures: features })\n }\n let msg = 'Forbidden'\n if (aclData && typeof aclData === 'object') {\n if (typeof aclData.error === 'string') {\n msg = aclData.error\n } else if (typeof aclData.message === 'string') {\n msg = aclData.message\n }\n } else {\n msg = await res.clone().text().catch(() => 'Forbidden')\n }\n throw new ForbiddenError(msg)\n }\n // If already on login, just return the response for the caller to handle\n }\n try {\n const header = res.headers.get('x-om-operation')\n const metadata = deserializeOperationMetadata(header)\n if (metadata) pushOperation(metadata)\n } catch {\n // ignore malformed headers\n }\n try {\n const warningRaw = res.headers.get('x-om-partial-index')\n if (warningRaw) {\n const parsed = JSON.parse(warningRaw) as Record<string, unknown>\n if (parsed && typeof parsed === 'object' && parsed.type === 'partial_index') {\n const entity = typeof parsed.entity === 'string' ? parsed.entity : String(parsed.entity ?? '')\n if (entity) {\n const baseCount = typeof parsed.baseCount === 'number' ? parsed.baseCount : null\n const indexedCount = typeof parsed.indexedCount === 'number' ? parsed.indexedCount : null\n const scope = parsed.scope === 'global' ? 'global' : 'scoped'\n const entityLabel =\n typeof parsed.entityLabel === 'string' && parsed.entityLabel.trim()\n ? parsed.entityLabel.trim()\n : entity\n pushPartialIndexWarning({ entity, entityLabel, baseCount, indexedCount, scope })\n }\n }\n }\n } catch {\n // ignore malformed headers\n }\n return res\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -96,7 +96,9 @@ function normalizeCrudServerError(err) {
|
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
98
|
if (typeof current !== "object") continue;
|
|
99
|
-
const
|
|
99
|
+
const errorField = current.error;
|
|
100
|
+
const nestedErrorMessage = errorField && typeof errorField === "object" && typeof errorField.message === "string" && errorField.message.trim() ? errorField.message : void 0;
|
|
101
|
+
const candidateMessage = typeof errorField === "string" ? errorField : typeof current.message === "string" ? current.message : nestedErrorMessage;
|
|
100
102
|
if (candidateMessage && !message) message = candidateMessage;
|
|
101
103
|
for (const key of JSON_FIELD_KEYS) {
|
|
102
104
|
const value = current[key];
|
|
@@ -198,7 +200,9 @@ async function raiseCrudError(res, fallbackMessage) {
|
|
|
198
200
|
const parsed = trimmed ? tryParseJson(trimmed) : null;
|
|
199
201
|
if (parsed && typeof parsed === "object") {
|
|
200
202
|
const data = parsed;
|
|
201
|
-
const
|
|
203
|
+
const normalized = normalizeCrudServerError(data);
|
|
204
|
+
const normalizedMessage = normalized.message?.trim();
|
|
205
|
+
const rawMessage = normalizedMessage && normalizedMessage.length > 0 ? normalizedMessage : fallbackMessage ?? `Request failed (${res.status})`;
|
|
202
206
|
const message2 = parseServerMessage(rawMessage);
|
|
203
207
|
throw buildHttpError(message2, {
|
|
204
208
|
...data,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/serverErrors.ts"],
|
|
4
|
-
"sourcesContent": ["export type CrudServerFieldErrors = Record<string, string>\n\nexport type NormalizedCrudServerError = {\n message?: string\n fieldErrors?: CrudServerFieldErrors\n details?: unknown\n status?: number\n raw?: string | null\n [key: string]: unknown\n}\n\nconst JSON_FIELD_KEYS = ['fieldErrors', 'fields', 'errors', 'data'] as const\nconst ISSUE_KEYS = ['details', 'issues', 'errors'] as const\n\nfunction coerceFieldErrors(input: unknown): CrudServerFieldErrors | null {\n if (!input || typeof input !== 'object') return null\n const result: CrudServerFieldErrors = {}\n for (const [rawKey, rawValue] of Object.entries(input as Record<string, unknown>)) {\n const key = typeof rawKey === 'string' && rawKey.trim().length > 0 ? rawKey.trim() : null\n if (!key) continue\n if (rawValue === undefined || rawValue === null) continue\n const message =\n typeof rawValue === 'string'\n ? rawValue\n : typeof (rawValue as any)?.message === 'string'\n ? (rawValue as any).message\n : String(rawValue)\n if (!message) continue\n result[key] = message\n }\n return Object.keys(result).length ? result : null\n}\n\nfunction mapIssueArray(issues: unknown): CrudServerFieldErrors | null {\n if (!Array.isArray(issues)) return null\n const result: CrudServerFieldErrors = {}\n for (const issue of issues) {\n if (!issue || typeof issue !== 'object') continue\n const pathValue: unknown[] = Array.isArray((issue as any).path) ? (issue as any).path : []\n let field: string | null = null\n for (const part of pathValue) {\n if (typeof part === 'string' && part.trim().length > 0) {\n field = part.trim()\n break\n }\n }\n if (!field && typeof (issue as any).field === 'string') {\n const fromField = ((issue as any).field as string).trim()\n field = fromField.length > 0 ? fromField : null\n }\n if (!field && pathValue.length > 0) {\n const joined = pathValue.map((part) => String(part)).join('.')\n if (joined) field = joined\n }\n if (!field) continue\n const message = typeof (issue as any).message === 'string' ? (issue as any).message : null\n if (!message) continue\n result[field] = message\n }\n return Object.keys(result).length ? result : null\n}\n\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return null\n }\n}\n\nfunction collectCandidatePayloads(err: unknown): unknown[] {\n const candidates: unknown[] = []\n if (!err) return candidates\n candidates.push(err)\n\n if (typeof err === 'string') {\n const parsed = tryParseJson(err)\n if (parsed) candidates.push(parsed)\n } else if (err instanceof Error) {\n if (typeof err.message === 'string' && err.message.trim()) {\n const parsed = tryParseJson(err.message.trim())\n if (parsed) candidates.push(parsed)\n }\n if ((err as any).cause) {\n candidates.push((err as any).cause)\n }\n } else if (typeof err === 'object') {\n const maybeMessage = (err as any)?.message\n if (typeof maybeMessage === 'string') {\n const parsed = tryParseJson(maybeMessage)\n if (parsed) candidates.push(parsed)\n }\n if ((err as any)?.body) candidates.push((err as any).body)\n if ((err as any)?.response) candidates.push((err as any).response)\n if ((err as any)?.data) candidates.push((err as any).data)\n }\n\n return candidates\n}\n\nexport function normalizeCrudServerError(err: unknown): NormalizedCrudServerError {\n let message: string | undefined\n let fieldErrors: CrudServerFieldErrors | undefined\n const processed = new Set<unknown>()\n\n const queue = collectCandidatePayloads(err)\n while (queue.length) {\n const current = queue.shift()\n if (!current || processed.has(current)) continue\n processed.add(current)\n\n if (typeof current === 'string') {\n if (!message) message = current\n const parsed = tryParseJson(current)\n if (parsed) queue.push(parsed)\n continue\n }\n\n if (current instanceof Response) {\n const body = (current as any)?._bodyInit\n if (body) queue.push(body)\n continue\n }\n\n if (typeof current !== 'object') continue\n\n const candidateMessage =\n typeof (current as any).error === 'string'\n ? (current as any).error\n : typeof (current as any).message === 'string'\n ? (current as any).message\n : undefined\n if (candidateMessage && !message) message = candidateMessage\n\n for (const key of JSON_FIELD_KEYS) {\n const value = (current as any)[key]\n if (value && typeof value === 'object') {\n const mapped = coerceFieldErrors(value)\n if (mapped) {\n fieldErrors = { ...(fieldErrors || {}), ...mapped }\n }\n }\n }\n\n for (const key of ISSUE_KEYS) {\n const value = (current as any)[key]\n const mapped = mapIssueArray(value)\n if (mapped) {\n fieldErrors = { ...(fieldErrors || {}), ...mapped }\n }\n }\n\n const nestedKeys = ['body', 'response', 'data', 'details']\n for (const nestedKey of nestedKeys) {\n const nested = (current as any)[nestedKey]\n if (nested && !processed.has(nested)) queue.push(nested)\n }\n }\n\n if (!message && fieldErrors && Object.keys(fieldErrors).length === 1) {\n const [, firstMessage] = Object.entries(fieldErrors)[0]\n message = firstMessage\n }\n\n if (!message && err instanceof Error && err.message) {\n message = err.message\n } else if (!message && typeof err === 'string') {\n message = err\n }\n\n return { message, fieldErrors }\n}\n\nexport type FieldNameMapperOptions = {\n customEntity?: boolean\n}\n\nexport function mapServerFieldNameToFormId(field: string, options?: FieldNameMapperOptions): string {\n const trimmed = field.trim()\n const customEntity = !!options?.customEntity\n if (customEntity) {\n if (trimmed.startsWith('cf_')) return trimmed.slice(3)\n if (trimmed.startsWith('cf:')) return trimmed.slice(3)\n return trimmed\n }\n if (trimmed.startsWith('cf_')) return trimmed\n if (trimmed.startsWith('cf:')) return `cf_${trimmed.slice(3)}`\n return trimmed\n}\n\nexport function mapCrudServerErrorToFormErrors(\n err: unknown,\n options?: FieldNameMapperOptions,\n): { message?: string; fieldErrors?: CrudServerFieldErrors } {\n const normalized = normalizeCrudServerError(err)\n const fieldErrors = normalized.fieldErrors\n if (!fieldErrors) return { message: normalized.message }\n\n const mapped: CrudServerFieldErrors = {}\n for (const [key, value] of Object.entries(fieldErrors)) {\n const formId = mapServerFieldNameToFormId(key, options)\n if (!formId) continue\n mapped[formId] = value\n }\n\n let message = normalized.message\n const firstEntry = Object.entries(mapped)[0]\n if (\n firstEntry &&\n (!message || (typeof message === 'string' && message.trim().toLowerCase() === 'invalid input'))\n ) {\n const [, fieldMessage] = firstEntry\n if (typeof fieldMessage === 'string' && fieldMessage.trim().length) {\n message = fieldMessage\n }\n }\n\n return {\n message,\n fieldErrors: mapped,\n }\n}\n\nexport function parseServerMessage(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return trimmed\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n try {\n const parsed = JSON.parse(trimmed) as Record<string, unknown>\n const text =\n typeof parsed?.error === 'string' && parsed.error.trim()\n ? parsed.error.trim()\n : typeof parsed?.message === 'string' && parsed.message.trim()\n ? parsed.message.trim()\n : null\n if (text) return text\n } catch {\n // ignore JSON parse failure, fall through to trimmed string\n }\n }\n return trimmed\n}\n\nfunction buildHttpError(\n message: string,\n extras?: Record<string, unknown>,\n): Error & Record<string, unknown> {\n const error = new Error(message) as Error & Record<string, unknown>\n if (!extras) return error\n for (const [key, value] of Object.entries(extras)) {\n error[key] = value\n }\n return error\n}\n\nexport async function raiseCrudError(res: Response, fallbackMessage?: string): Promise<never> {\n let raw: string | null = null\n try {\n raw = await res.text()\n } catch {\n raw = null\n }\n\n const trimmed = raw && raw.trim() ? raw.trim() : null\n const parsed = trimmed ? tryParseJson(trimmed) : null\n\n if (parsed && typeof parsed === 'object') {\n const data = parsed as Record<string, unknown>\n const rawMessage =\n typeof data.error === 'string' && data.error.trim()\n ? data.error.trim()\n : typeof data.message === 'string' && data.message.trim()\n ? data.message.trim()\n : fallbackMessage ?? `Request failed (${res.status})`\n const message = parseServerMessage(rawMessage)\n throw buildHttpError(message, {\n ...data,\n status: res.status,\n raw: trimmed ?? null,\n })\n }\n\n const message = parseServerMessage(fallbackMessage ?? `Request failed (${res.status})`)\n throw buildHttpError(message, { status: res.status, raw: trimmed ?? null })\n}\n\nexport type CrudFormError = Error & {\n status?: number\n fieldErrors?: CrudServerFieldErrors\n details?: unknown\n}\n\nexport function createCrudFormError(\n message: string,\n fieldErrors?: CrudServerFieldErrors,\n extras?: Partial<Pick<CrudFormError, 'status' | 'details'>>,\n): CrudFormError {\n const error = new Error(message) as CrudFormError\n if (fieldErrors && Object.keys(fieldErrors).length) error.fieldErrors = fieldErrors\n if (extras?.status !== undefined) error.status = extras.status\n if (extras?.details !== undefined) error.details = extras.details\n return error\n}\n\nexport async function readJsonSafe<T>(res: Response, fallback: T | null = null): Promise<T | null> {\n try {\n const text = await res.text()\n if (!text) return fallback\n return JSON.parse(text) as T\n } catch {\n return fallback\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["export type CrudServerFieldErrors = Record<string, string>\n\nexport type NormalizedCrudServerError = {\n message?: string\n fieldErrors?: CrudServerFieldErrors\n details?: unknown\n status?: number\n raw?: string | null\n [key: string]: unknown\n}\n\nconst JSON_FIELD_KEYS = ['fieldErrors', 'fields', 'errors', 'data'] as const\nconst ISSUE_KEYS = ['details', 'issues', 'errors'] as const\n\nfunction coerceFieldErrors(input: unknown): CrudServerFieldErrors | null {\n if (!input || typeof input !== 'object') return null\n const result: CrudServerFieldErrors = {}\n for (const [rawKey, rawValue] of Object.entries(input as Record<string, unknown>)) {\n const key = typeof rawKey === 'string' && rawKey.trim().length > 0 ? rawKey.trim() : null\n if (!key) continue\n if (rawValue === undefined || rawValue === null) continue\n const message =\n typeof rawValue === 'string'\n ? rawValue\n : typeof (rawValue as any)?.message === 'string'\n ? (rawValue as any).message\n : String(rawValue)\n if (!message) continue\n result[key] = message\n }\n return Object.keys(result).length ? result : null\n}\n\nfunction mapIssueArray(issues: unknown): CrudServerFieldErrors | null {\n if (!Array.isArray(issues)) return null\n const result: CrudServerFieldErrors = {}\n for (const issue of issues) {\n if (!issue || typeof issue !== 'object') continue\n const pathValue: unknown[] = Array.isArray((issue as any).path) ? (issue as any).path : []\n let field: string | null = null\n for (const part of pathValue) {\n if (typeof part === 'string' && part.trim().length > 0) {\n field = part.trim()\n break\n }\n }\n if (!field && typeof (issue as any).field === 'string') {\n const fromField = ((issue as any).field as string).trim()\n field = fromField.length > 0 ? fromField : null\n }\n if (!field && pathValue.length > 0) {\n const joined = pathValue.map((part) => String(part)).join('.')\n if (joined) field = joined\n }\n if (!field) continue\n const message = typeof (issue as any).message === 'string' ? (issue as any).message : null\n if (!message) continue\n result[field] = message\n }\n return Object.keys(result).length ? result : null\n}\n\nfunction tryParseJson(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return null\n }\n}\n\nfunction collectCandidatePayloads(err: unknown): unknown[] {\n const candidates: unknown[] = []\n if (!err) return candidates\n candidates.push(err)\n\n if (typeof err === 'string') {\n const parsed = tryParseJson(err)\n if (parsed) candidates.push(parsed)\n } else if (err instanceof Error) {\n if (typeof err.message === 'string' && err.message.trim()) {\n const parsed = tryParseJson(err.message.trim())\n if (parsed) candidates.push(parsed)\n }\n if ((err as any).cause) {\n candidates.push((err as any).cause)\n }\n } else if (typeof err === 'object') {\n const maybeMessage = (err as any)?.message\n if (typeof maybeMessage === 'string') {\n const parsed = tryParseJson(maybeMessage)\n if (parsed) candidates.push(parsed)\n }\n if ((err as any)?.body) candidates.push((err as any).body)\n if ((err as any)?.response) candidates.push((err as any).response)\n if ((err as any)?.data) candidates.push((err as any).data)\n }\n\n return candidates\n}\n\nexport function normalizeCrudServerError(err: unknown): NormalizedCrudServerError {\n let message: string | undefined\n let fieldErrors: CrudServerFieldErrors | undefined\n const processed = new Set<unknown>()\n\n const queue = collectCandidatePayloads(err)\n while (queue.length) {\n const current = queue.shift()\n if (!current || processed.has(current)) continue\n processed.add(current)\n\n if (typeof current === 'string') {\n if (!message) message = current\n const parsed = tryParseJson(current)\n if (parsed) queue.push(parsed)\n continue\n }\n\n if (current instanceof Response) {\n const body = (current as any)?._bodyInit\n if (body) queue.push(body)\n continue\n }\n\n if (typeof current !== 'object') continue\n\n const errorField = (current as any).error\n const nestedErrorMessage =\n errorField && typeof errorField === 'object' &&\n typeof (errorField as any).message === 'string' &&\n (errorField as any).message.trim()\n ? ((errorField as any).message as string)\n : undefined\n const candidateMessage =\n typeof errorField === 'string'\n ? errorField\n : typeof (current as any).message === 'string'\n ? (current as any).message\n : nestedErrorMessage\n if (candidateMessage && !message) message = candidateMessage\n\n for (const key of JSON_FIELD_KEYS) {\n const value = (current as any)[key]\n if (value && typeof value === 'object') {\n const mapped = coerceFieldErrors(value)\n if (mapped) {\n fieldErrors = { ...(fieldErrors || {}), ...mapped }\n }\n }\n }\n\n for (const key of ISSUE_KEYS) {\n const value = (current as any)[key]\n const mapped = mapIssueArray(value)\n if (mapped) {\n fieldErrors = { ...(fieldErrors || {}), ...mapped }\n }\n }\n\n const nestedKeys = ['body', 'response', 'data', 'details']\n for (const nestedKey of nestedKeys) {\n const nested = (current as any)[nestedKey]\n if (nested && !processed.has(nested)) queue.push(nested)\n }\n }\n\n if (!message && fieldErrors && Object.keys(fieldErrors).length === 1) {\n const [, firstMessage] = Object.entries(fieldErrors)[0]\n message = firstMessage\n }\n\n if (!message && err instanceof Error && err.message) {\n message = err.message\n } else if (!message && typeof err === 'string') {\n message = err\n }\n\n return { message, fieldErrors }\n}\n\nexport type FieldNameMapperOptions = {\n customEntity?: boolean\n}\n\nexport function mapServerFieldNameToFormId(field: string, options?: FieldNameMapperOptions): string {\n const trimmed = field.trim()\n const customEntity = !!options?.customEntity\n if (customEntity) {\n if (trimmed.startsWith('cf_')) return trimmed.slice(3)\n if (trimmed.startsWith('cf:')) return trimmed.slice(3)\n return trimmed\n }\n if (trimmed.startsWith('cf_')) return trimmed\n if (trimmed.startsWith('cf:')) return `cf_${trimmed.slice(3)}`\n return trimmed\n}\n\nexport function mapCrudServerErrorToFormErrors(\n err: unknown,\n options?: FieldNameMapperOptions,\n): { message?: string; fieldErrors?: CrudServerFieldErrors } {\n const normalized = normalizeCrudServerError(err)\n const fieldErrors = normalized.fieldErrors\n if (!fieldErrors) return { message: normalized.message }\n\n const mapped: CrudServerFieldErrors = {}\n for (const [key, value] of Object.entries(fieldErrors)) {\n const formId = mapServerFieldNameToFormId(key, options)\n if (!formId) continue\n mapped[formId] = value\n }\n\n let message = normalized.message\n const firstEntry = Object.entries(mapped)[0]\n if (\n firstEntry &&\n (!message || (typeof message === 'string' && message.trim().toLowerCase() === 'invalid input'))\n ) {\n const [, fieldMessage] = firstEntry\n if (typeof fieldMessage === 'string' && fieldMessage.trim().length) {\n message = fieldMessage\n }\n }\n\n return {\n message,\n fieldErrors: mapped,\n }\n}\n\nexport function parseServerMessage(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return trimmed\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n try {\n const parsed = JSON.parse(trimmed) as Record<string, unknown>\n const text =\n typeof parsed?.error === 'string' && parsed.error.trim()\n ? parsed.error.trim()\n : typeof parsed?.message === 'string' && parsed.message.trim()\n ? parsed.message.trim()\n : null\n if (text) return text\n } catch {\n // ignore JSON parse failure, fall through to trimmed string\n }\n }\n return trimmed\n}\n\nfunction buildHttpError(\n message: string,\n extras?: Record<string, unknown>,\n): Error & Record<string, unknown> {\n const error = new Error(message) as Error & Record<string, unknown>\n if (!extras) return error\n for (const [key, value] of Object.entries(extras)) {\n error[key] = value\n }\n return error\n}\n\nexport async function raiseCrudError(res: Response, fallbackMessage?: string): Promise<never> {\n let raw: string | null = null\n try {\n raw = await res.text()\n } catch {\n raw = null\n }\n\n const trimmed = raw && raw.trim() ? raw.trim() : null\n const parsed = trimmed ? tryParseJson(trimmed) : null\n\n if (parsed && typeof parsed === 'object') {\n const data = parsed as Record<string, unknown>\n const normalized = normalizeCrudServerError(data)\n const normalizedMessage = normalized.message?.trim()\n const rawMessage =\n normalizedMessage && normalizedMessage.length > 0\n ? normalizedMessage\n : fallbackMessage ?? `Request failed (${res.status})`\n const message = parseServerMessage(rawMessage)\n throw buildHttpError(message, {\n ...data,\n status: res.status,\n raw: trimmed ?? null,\n })\n }\n\n const message = parseServerMessage(fallbackMessage ?? `Request failed (${res.status})`)\n throw buildHttpError(message, { status: res.status, raw: trimmed ?? null })\n}\n\nexport type CrudFormError = Error & {\n status?: number\n fieldErrors?: CrudServerFieldErrors\n details?: unknown\n}\n\nexport function createCrudFormError(\n message: string,\n fieldErrors?: CrudServerFieldErrors,\n extras?: Partial<Pick<CrudFormError, 'status' | 'details'>>,\n): CrudFormError {\n const error = new Error(message) as CrudFormError\n if (fieldErrors && Object.keys(fieldErrors).length) error.fieldErrors = fieldErrors\n if (extras?.status !== undefined) error.status = extras.status\n if (extras?.details !== undefined) error.details = extras.details\n return error\n}\n\nexport async function readJsonSafe<T>(res: Response, fallback: T | null = null): Promise<T | null> {\n try {\n const text = await res.text()\n if (!text) return fallback\n return JSON.parse(text) as T\n } catch {\n return fallback\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["message"]
|
|
7
7
|
}
|
|
@@ -45,6 +45,7 @@ function useAutoDiscoveredFields({
|
|
|
45
45
|
type,
|
|
46
46
|
group: meta?.filterGroup ?? meta?.columnChooserGroup
|
|
47
47
|
};
|
|
48
|
+
if (meta?.filterIconName) field.iconName = meta.filterIconName;
|
|
48
49
|
if (meta?.filterOptions) field.options = meta.filterOptions;
|
|
49
50
|
if (meta?.filterLoadOptions) field.loadOptions = meta.filterLoadOptions;
|
|
50
51
|
filterFields.push(field);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/useAutoDiscoveredFields.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport type { FilterFieldDef as AdvancedFilterFieldDef, FilterFieldType, FilterOption } from '@open-mercato/shared/lib/query/advanced-filter'\nimport type { ColumnChooserField } from '../columns/ColumnChooserPanel'\nimport type { CustomFieldDefDto } from './customFieldDefs'\nimport {\n mapCustomFieldKindToFilterType,\n normalizeCustomFieldFilterOptions,\n supportsCustomFieldColumn,\n} from './customFieldColumns'\n\ntype ColumnMeta = {\n filterKey?: string\n filterType?: FilterFieldType\n filterOptions?: FilterOption[]\n filterLoadOptions?: (query?: string) => Promise<FilterOption[]>\n filterable?: boolean\n filterGroup?: string\n columnChooserGroup?: string\n alwaysVisible?: boolean\n hidden?: boolean\n truncate?: boolean\n maxWidth?: string\n priority?: number\n tooltipContent?: (row: unknown) => string | undefined\n}\n\nfunction resolveHeaderLabel(column: ColumnDef<any, any>): string {\n const header = (column as any).header\n if (typeof header === 'string') return header\n const accessorKey = (column as any).accessorKey as string | undefined\n if (!accessorKey) return ''\n return accessorKey\n .replace(/^cf_/, '')\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (c: string) => c.toUpperCase())\n}\n\nfunction inferFilterType(accessorKey: string, meta?: ColumnMeta): FilterFieldType {\n if (meta?.filterType) return meta.filterType\n if (accessorKey.endsWith('_at') || accessorKey === 'createdAt' || accessorKey === 'updatedAt' || accessorKey === 'expectedCloseAt' || accessorKey.endsWith('At')) return 'date'\n if (accessorKey === 'probability' || accessorKey === 'valueAmount' || accessorKey === 'value_amount') return 'number'\n if (meta?.filterOptions) return 'select'\n return 'text'\n}\n\nexport type UseAutoDiscoveredFieldsInput = {\n columns: ColumnDef<any, any>[]\n customFieldDefs: CustomFieldDefDto[]\n}\n\nexport type UseAutoDiscoveredFieldsResult = {\n advancedFilterFields: AdvancedFilterFieldDef[]\n columnChooserFields: ColumnChooserField[]\n}\n\nexport function useAutoDiscoveredFields({\n columns,\n customFieldDefs,\n}: UseAutoDiscoveredFieldsInput): UseAutoDiscoveredFieldsResult {\n return React.useMemo(() => {\n const filterFields: AdvancedFilterFieldDef[] = []\n const chooserFields: ColumnChooserField[] = []\n const seenFilterKeys = new Set<string>()\n const seenChooserKeys = new Set<string>()\n\n for (let i = 0; i < columns.length; i++) {\n const col = columns[i]\n const accessorKey = (col as any).accessorKey as string | undefined\n if (!accessorKey) continue\n\n const meta = (col as any).meta as ColumnMeta | undefined\n const label = resolveHeaderLabel(col)\n if (!label) continue\n const filterKey = meta?.filterKey ?? accessorKey\n\n // Advanced filter field\n if (meta?.filterable !== false && filterKey && !seenFilterKeys.has(filterKey)) {\n seenFilterKeys.add(filterKey)\n const type = inferFilterType(filterKey, meta)\n const field: AdvancedFilterFieldDef = {\n key: filterKey,\n label,\n type,\n group: meta?.filterGroup ?? meta?.columnChooserGroup,\n }\n if (meta?.filterOptions) field.options = meta.filterOptions\n if (meta?.filterLoadOptions) field.loadOptions = meta.filterLoadOptions\n filterFields.push(field)\n }\n\n // Column chooser field\n if (!seenChooserKeys.has(accessorKey)) {\n seenChooserKeys.add(accessorKey)\n chooserFields.push({\n key: accessorKey,\n label,\n group: meta?.columnChooserGroup ?? 'Columns',\n alwaysVisible: meta?.alwaysVisible ?? i === 0,\n defaultVisible: true,\n })\n }\n }\n\n // Append ALL custom field definitions (not just filterable/listVisible)\n // so that every entity field appears in both the column chooser and advanced filter\n for (const def of customFieldDefs) {\n if (!supportsCustomFieldColumn(def)) continue\n const filterKey = `cf_${def.key}`\n if (!seenFilterKeys.has(filterKey)) {\n seenFilterKeys.add(filterKey)\n const type = mapCustomFieldKindToFilterType(def.kind)\n const field: AdvancedFilterFieldDef = {\n key: filterKey,\n label: def.label || def.key,\n type,\n group: 'Custom Fields',\n }\n if (type === 'select' && Array.isArray(def.options) && def.options.length) {\n field.options = normalizeCustomFieldFilterOptions(def.options)\n }\n filterFields.push(field)\n }\n if (!seenChooserKeys.has(filterKey)) {\n seenChooserKeys.add(filterKey)\n chooserFields.push({\n key: filterKey,\n label: def.label || def.key,\n group: def.group?.title ?? 'Custom Fields',\n defaultVisible: false,\n })\n }\n }\n\n return { advancedFilterFields: filterFields, columnChooserFields: chooserFields }\n }, [columns, customFieldDefs])\n}\n"],
|
|
5
|
-
"mappings": ";AACA,YAAY,WAAW;AAKvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport type { FilterFieldDef as AdvancedFilterFieldDef, FilterFieldType, FilterOption } from '@open-mercato/shared/lib/query/advanced-filter'\nimport type { ColumnChooserField } from '../columns/ColumnChooserPanel'\nimport type { CustomFieldDefDto } from './customFieldDefs'\nimport {\n mapCustomFieldKindToFilterType,\n normalizeCustomFieldFilterOptions,\n supportsCustomFieldColumn,\n} from './customFieldColumns'\n\ntype ColumnMeta = {\n filterKey?: string\n filterType?: FilterFieldType\n filterOptions?: FilterOption[]\n filterLoadOptions?: (query?: string) => Promise<FilterOption[]>\n filterable?: boolean\n filterGroup?: string\n filterIconName?: string\n columnChooserGroup?: string\n alwaysVisible?: boolean\n hidden?: boolean\n truncate?: boolean\n maxWidth?: string\n priority?: number\n tooltipContent?: (row: unknown) => string | undefined\n}\n\nfunction resolveHeaderLabel(column: ColumnDef<any, any>): string {\n const header = (column as any).header\n if (typeof header === 'string') return header\n const accessorKey = (column as any).accessorKey as string | undefined\n if (!accessorKey) return ''\n return accessorKey\n .replace(/^cf_/, '')\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (c: string) => c.toUpperCase())\n}\n\nfunction inferFilterType(accessorKey: string, meta?: ColumnMeta): FilterFieldType {\n if (meta?.filterType) return meta.filterType\n if (accessorKey.endsWith('_at') || accessorKey === 'createdAt' || accessorKey === 'updatedAt' || accessorKey === 'expectedCloseAt' || accessorKey.endsWith('At')) return 'date'\n if (accessorKey === 'probability' || accessorKey === 'valueAmount' || accessorKey === 'value_amount') return 'number'\n if (meta?.filterOptions) return 'select'\n return 'text'\n}\n\nexport type UseAutoDiscoveredFieldsInput = {\n columns: ColumnDef<any, any>[]\n customFieldDefs: CustomFieldDefDto[]\n}\n\nexport type UseAutoDiscoveredFieldsResult = {\n advancedFilterFields: AdvancedFilterFieldDef[]\n columnChooserFields: ColumnChooserField[]\n}\n\nexport function useAutoDiscoveredFields({\n columns,\n customFieldDefs,\n}: UseAutoDiscoveredFieldsInput): UseAutoDiscoveredFieldsResult {\n return React.useMemo(() => {\n const filterFields: AdvancedFilterFieldDef[] = []\n const chooserFields: ColumnChooserField[] = []\n const seenFilterKeys = new Set<string>()\n const seenChooserKeys = new Set<string>()\n\n for (let i = 0; i < columns.length; i++) {\n const col = columns[i]\n const accessorKey = (col as any).accessorKey as string | undefined\n if (!accessorKey) continue\n\n const meta = (col as any).meta as ColumnMeta | undefined\n const label = resolveHeaderLabel(col)\n if (!label) continue\n const filterKey = meta?.filterKey ?? accessorKey\n\n // Advanced filter field\n if (meta?.filterable !== false && filterKey && !seenFilterKeys.has(filterKey)) {\n seenFilterKeys.add(filterKey)\n const type = inferFilterType(filterKey, meta)\n const field: AdvancedFilterFieldDef = {\n key: filterKey,\n label,\n type,\n group: meta?.filterGroup ?? meta?.columnChooserGroup,\n }\n if (meta?.filterIconName) field.iconName = meta.filterIconName\n if (meta?.filterOptions) field.options = meta.filterOptions\n if (meta?.filterLoadOptions) field.loadOptions = meta.filterLoadOptions\n filterFields.push(field)\n }\n\n // Column chooser field\n if (!seenChooserKeys.has(accessorKey)) {\n seenChooserKeys.add(accessorKey)\n chooserFields.push({\n key: accessorKey,\n label,\n group: meta?.columnChooserGroup ?? 'Columns',\n alwaysVisible: meta?.alwaysVisible ?? i === 0,\n defaultVisible: true,\n })\n }\n }\n\n // Append ALL custom field definitions (not just filterable/listVisible)\n // so that every entity field appears in both the column chooser and advanced filter\n for (const def of customFieldDefs) {\n if (!supportsCustomFieldColumn(def)) continue\n const filterKey = `cf_${def.key}`\n if (!seenFilterKeys.has(filterKey)) {\n seenFilterKeys.add(filterKey)\n const type = mapCustomFieldKindToFilterType(def.kind)\n const field: AdvancedFilterFieldDef = {\n key: filterKey,\n label: def.label || def.key,\n type,\n group: 'Custom Fields',\n }\n if (type === 'select' && Array.isArray(def.options) && def.options.length) {\n field.options = normalizeCustomFieldFilterOptions(def.options)\n }\n filterFields.push(field)\n }\n if (!seenChooserKeys.has(filterKey)) {\n seenChooserKeys.add(filterKey)\n chooserFields.push({\n key: filterKey,\n label: def.label || def.key,\n group: def.group?.title ?? 'Custom Fields',\n defaultVisible: false,\n })\n }\n }\n\n return { advancedFilterFields: filterFields, columnChooserFields: chooserFields }\n }, [columns, customFieldDefs])\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { apiCall } from "./apiCall.js";
|
|
4
|
+
function useCurrentUserId() {
|
|
5
|
+
const [userId, setUserId] = React.useState("");
|
|
6
|
+
React.useEffect(() => {
|
|
7
|
+
let cancelled = false;
|
|
8
|
+
void (async () => {
|
|
9
|
+
try {
|
|
10
|
+
const res = await apiCall("/api/auth/feature-check", {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "content-type": "application/json" },
|
|
13
|
+
body: JSON.stringify({ features: [] })
|
|
14
|
+
});
|
|
15
|
+
if (!cancelled && res.ok && typeof res.result?.userId === "string") {
|
|
16
|
+
setUserId(res.result.userId);
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
if (!cancelled) setUserId("");
|
|
20
|
+
}
|
|
21
|
+
})();
|
|
22
|
+
return () => {
|
|
23
|
+
cancelled = true;
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
return userId;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
useCurrentUserId
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=useCurrentUserId.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/utils/useCurrentUserId.ts"],
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { apiCall } from './apiCall'\n\ntype FeatureCheckResponse = {\n ok: boolean\n granted: string[]\n userId: string\n}\n\n/**\n * Read the current user's id on the client. Returns `''` until the request resolves.\n *\n * Implementation note: there is no first-class `/api/auth/me` endpoint yet, so this\n * piggy-backs on `/api/auth/feature-check` with an empty `features` array \u2014 the response\n * always carries the JWT `userId`. Replace with a dedicated `/api/auth/me` or an\n * SSR-bootstrapped user context once available, and remove this hop.\n */\nexport function useCurrentUserId(): string {\n const [userId, setUserId] = React.useState<string>('')\n React.useEffect(() => {\n let cancelled = false\n void (async () => {\n try {\n const res = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: [] }),\n })\n if (!cancelled && res.ok && typeof res.result?.userId === 'string') {\n setUserId(res.result.userId)\n }\n } catch {\n if (!cancelled) setUserId('')\n }\n })()\n return () => { cancelled = true }\n }, [])\n return userId\n}\n"],
|
|
5
|
+
"mappings": ";AACA,YAAY,WAAW;AACvB,SAAS,eAAe;AAgBjB,SAAS,mBAA2B;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAiB,EAAE;AACrD,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,MAAM,MAAM,QAA8B,2BAA2B;AAAA,UACzE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC,EAAE,CAAC;AAAA,QACvC,CAAC;AACD,YAAI,CAAC,aAAa,IAAI,MAAM,OAAO,IAAI,QAAQ,WAAW,UAAU;AAClE,oBAAU,IAAI,OAAO,MAAM;AAAA,QAC7B;AAAA,MACF,QAAQ;AACN,YAAI,CAAC,UAAW,WAAU,EAAE;AAAA,MAC9B;AAAA,IACF,GAAG;AACH,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAClC,GAAG,CAAC,CAAC;AACL,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|