shared currency field + expenses rendered from react-openapi fields
- add CurrencyField renderer (inherits NumberField for editing) with cached Intl formatter; route via uiType "currency" in list/detail/form - add resourceConfig.fieldTypes override applied in AppProvider, so amount becomes currency for both Admin and the Expenses page - render Expenses page through react-openapi fields (ListCellRenderer, DetailFieldRenderer, applyDisplayFormat) instead of custom fields - resolve relative /uploads entity logos against the API base URL on load so entity logos render on the expenses feed - drop custom avatar, currency/date formatters from the Expense module
This commit is contained in:
@@ -5,6 +5,7 @@ export { useAppContext } from "./src/context/AppContext";
|
||||
export { useResource } from "./src/context/useResource";
|
||||
export { ListCellRenderer, DetailFieldRenderer, applyDisplayFormat } from "./src/components/fields";
|
||||
export { FormFieldRenderer } from "./src/components/fields/FormFieldRenderer";
|
||||
export { CurrencyField, formatCurrency } from "./src/components/fields/renderers/CurrencyField";
|
||||
export { SseStreamView } from "./src/components/SseStreamView";
|
||||
export { SseConnectionStatus } from "./src/components/SseConnectionStatus";
|
||||
export { getApi } from "./src/hooks/useApi";
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import { Box, Typography, Avatar } from "@mui/material";
|
||||
import type { FieldConfig } from "../../types";
|
||||
import { ListCellRenderer } from "./ListCellRenderer";
|
||||
import { CurrencyField } from "./renderers/CurrencyField";
|
||||
|
||||
interface DetailFieldProps {
|
||||
field: FieldConfig;
|
||||
@@ -18,7 +19,9 @@ export function DetailFieldRenderer({ field, value, displayFormat, basePath }: D
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ mb: 0.25, display: "block" }}>
|
||||
{field.label}
|
||||
</Typography>
|
||||
{field.uiType === "image" ? (
|
||||
{field.uiType === "currency" ? (
|
||||
<CurrencyField value={Number(value)} large />
|
||||
) : field.uiType === "image" ? (
|
||||
<Avatar src={value} variant="rounded" sx={{ width: 120, height: 120 }} />
|
||||
) : (
|
||||
<ListCellRenderer field={field} value={value} displayFormat={displayFormat} basePath={basePath} />
|
||||
|
||||
@@ -96,6 +96,17 @@ export function FormFieldRenderer({ field, value, onChange, error, fkOptions, fk
|
||||
return <BooleanField field={field} value={value} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (field.uiType === "currency") {
|
||||
return (
|
||||
<NumberField
|
||||
field={field}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "integer" || field.type === "number") {
|
||||
return (
|
||||
<NumberField
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Box, Typography, Chip, Avatar, Dialog, DialogTitle, DialogContent, Dial
|
||||
import type { FieldConfig } from "../../types";
|
||||
import { applyDisplayFormat } from "./utils";
|
||||
import { InlineRefField } from "./renderers/InlineRefField";
|
||||
import { CurrencyField } from "./renderers/CurrencyField";
|
||||
import { extractFields } from "../../transformers/field-config";
|
||||
import { useAppContext } from "../../context/AppContext";
|
||||
|
||||
@@ -140,6 +141,10 @@ export function ListCellRenderer({ field, value, displayFormat, basePath }: List
|
||||
return <Avatar src={value} variant="rounded" sx={{ width: 40, height: 40 }} />;
|
||||
}
|
||||
|
||||
if (field.uiType === "currency" && value != null && !Number.isNaN(Number(value))) {
|
||||
return <CurrencyField value={Number(value)} />;
|
||||
}
|
||||
|
||||
if (field.type === "boolean") {
|
||||
return <Chip label={value ? "Yes" : "No"} size="small" color={value ? "success" : "default"} />;
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ export { ListCellRenderer } from "./ListCellRenderer";
|
||||
export { DetailFieldRenderer } from "./DetailFieldRenderer";
|
||||
export { applyDisplayFormat } from "./utils";
|
||||
export { JsonField } from "./renderers/JsonField";
|
||||
export { CurrencyField, formatCurrency } from "./renderers/CurrencyField";
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
const CURRENCIES = ["INR", "USD", "EUR", "GBP", "AED", "SGD"];
|
||||
const _currencyFormatters = new Map<string, Intl.NumberFormat>();
|
||||
|
||||
export function formatCurrency(amount: number, currency?: string): string {
|
||||
const code = currency && CURRENCIES.includes(currency) ? currency : "INR";
|
||||
let formatter = _currencyFormatters.get(code);
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat("en-IN", {
|
||||
style: "currency",
|
||||
currency: code,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
_currencyFormatters.set(code, formatter);
|
||||
}
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
interface CurrencyFieldProps {
|
||||
value: number;
|
||||
currency?: string;
|
||||
large?: boolean;
|
||||
}
|
||||
|
||||
export function CurrencyField({ value, currency, large }: CurrencyFieldProps) {
|
||||
const negative = value < 0;
|
||||
return (
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: large ? "0.9375rem" : "0.8125rem",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
color: negative ? "error.main" : "success.main",
|
||||
}}
|
||||
>
|
||||
{formatCurrency(value, currency)}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
@@ -43,6 +43,20 @@ function extractProfileOperations(spec: any): ProfileOperation[] {
|
||||
return ops;
|
||||
}
|
||||
|
||||
function applyResourceOverrides(configs: ResourceConfig[], specConfiguration: SpecConfiguration): ResourceConfig[] {
|
||||
for (const resource of configs) {
|
||||
const fieldTypes = specConfiguration.resourceConfig?.[resource.name]?.fieldTypes;
|
||||
if (!fieldTypes) continue;
|
||||
// fields and orderedFields share the same FieldConfig object references,
|
||||
// so an in-place mutation updates both.
|
||||
for (const field of resource.fields) {
|
||||
const override = fieldTypes[field.name];
|
||||
if (override) field.uiType = override;
|
||||
}
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
const DEFAULT_AUTH_CONFIG: AuthConfig = {
|
||||
serverUrl: "",
|
||||
loginPath: "/login",
|
||||
@@ -88,7 +102,7 @@ export function AppProvider({ specConfiguration, children }: AppProviderProps) {
|
||||
if (errs.length === 0) {
|
||||
const configs = buildResourceConfigs(spec);
|
||||
if (!cancelled) {
|
||||
setResources(configs);
|
||||
setResources(applyResourceOverrides(configs, specConfiguration));
|
||||
}
|
||||
|
||||
const baseUrl = specConfiguration.baseApiUrl ?? spec.servers?.[0]?.url ?? "";
|
||||
|
||||
@@ -4,6 +4,8 @@ export interface ResourceConfiguration {
|
||||
filterOptions?: {
|
||||
mode?: FilterMode;
|
||||
};
|
||||
/** Map of field name → uiType override (e.g. { amount: "currency" }). */
|
||||
fieldTypes?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ProfileComponents {
|
||||
|
||||
Reference in New Issue
Block a user