mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-31 07:28:04 +00:00
feat: new banking module (#54720)
* feat: initial SPA setup for banking * wip: bring over new banking module * feat: added Espresso design tokens * feat: button styles * fix: add all ink colors * wip: espresso design system changes * feat: button and badge espresso components * fix: button styling for reconcile * feat: Espresso progress bar * feat: Espresso toggle switch * feat: Espresso tabs design * fix: vertical tab support * fix: button sizing across modals * feat: Espresso style table layout * feat: Espresso tooltip * feat: Espresso elevations and checkbox * feat: Dialog with Espresso styles * feat: Espresso textarea * fix: input styles * fix: colors on bank picker * fix: breadcrumb styling * fix: bank picker styling * feat: create doctypes and fields for bank reconciliation * feat: APIs for banking * fix: use date format parser * fix: font styling to match Espresso * wip: settings modal * feat: settings dialog component * fix: icons and invalid requests * feat: preferences tab * fix: adjust icon stroke width to 1.5 * feat: rule configuration in settings * fix: remove sheet component * feat: alert and error banner component * feat: dropdown in Espresso * feat: popover and select in Espresso * fix: cleanup more styles * fix: match size of link fields * feat: command styling * fix: remove unused style tokens * fix: styles for global date picker dropdown * fix: styles for match and reconcile * feat: table Espresso component * feat: remove all other design tokens * fix: remove unused tokens * fix: form elements * fix: remove unused styles and fix filters in bank transaction list * feat: fetch bank rec doctypes for filtering * fix: record payment modal * feat: support for dark mode switching * fix: move bank logos to public folder * feat: add support for RTL * feat: support for RTL * chore: send layout direction in dev boot * fix: make checkbox work in RTL * feat: dark mode support * fix: dark mode style * feat: bank logos in dark mode * feat: dark mode bank logos * chore: use dark mode bank logos everywhere * chore: move rule evaluation to controller * chore: add tests for bank transaction rules * fix: move deps to fix actions errors * fix: move tw-animate-css to deps * fix: remove shadcn * fix: do not open modal if no transactions selected * fix: add translation strings * feat: add banner on existing bank reconciliation tool * feat: bank statement import * fix: translations and layout directions * fix: validation for transaction matching rule * fix: styles * fix: show conflicting transactions in alert * fix: show help text for new banking module forms * feat: show total debits and credits * fix: dark mode colors in automatic config * feat: add keyboard shortcuts help * feat: added keyboard shortcut for settings * fix: decrease size of progress bar * chore: bump packages * feat: add tests for statement import * fix: settings dialog * fix: show banner on small screens * fix: show banner when no bank account set
This commit is contained in:
273
banking/src/components/ui/settings-dialog.tsx
Normal file
273
banking/src/components/ui/settings-dialog.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import * as React from "react"
|
||||
import { Tabs as TabsPrimitive, Dialog as DialogPrimitive } from "radix-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { DialogContent } from "./dialog"
|
||||
|
||||
/**
|
||||
* Sample Usage:
|
||||
*
|
||||
* <Dialog open={open} onOpenChange={setOpen}>
|
||||
* <DialogTrigger>
|
||||
* ...your content...
|
||||
* </DialogTrigger>
|
||||
*
|
||||
* <SettingsDialog onClose={() => setOpen(false)} defaultValue="preferences">
|
||||
* <SettingsTabs>
|
||||
* <SettingsTabGroup header="Configuration">
|
||||
* <SettingsTabItem icon={<SlidersVerticalIcon />} label="Preferences" value="preferences" />
|
||||
* <SettingsTabItem icon={<ZapIcon />} label="Matching Rules" value="rules" />
|
||||
* </SettingsTabGroup>
|
||||
* <SettingsTabGroup header="Setup">
|
||||
* <SettingsTabItem icon={<LandmarkIcon />} label="Bank Accounts" value="bank-accounts" />
|
||||
* <SettingsTabItem icon={<ListIcon />} label="Masters" value="masters" />
|
||||
* </SettingsTabGroup>
|
||||
* </SettingsTabs>
|
||||
*
|
||||
* <SettingsPanels>
|
||||
* <SettingsPanel value="preferences"><Preferences /></SettingsPanel>
|
||||
* <SettingsPanel value="rules"><MatchingRules /></SettingsPanel>
|
||||
* <SettingsPanel value="bank-accounts"><BankAccounts /></SettingsPanel>
|
||||
* <SettingsPanel value="masters"><Masters /></SettingsPanel>
|
||||
* </SettingsPanels>
|
||||
* </SettingsDialog>
|
||||
* </Dialog>
|
||||
*/
|
||||
|
||||
type SettingsDialogContextValue = {
|
||||
onClose?: VoidFunction
|
||||
}
|
||||
|
||||
const SettingsDialogContext = React.createContext<SettingsDialogContextValue>({})
|
||||
|
||||
/**
|
||||
* Exposes `onClose` to descendant panels so they can dismiss the dialog after
|
||||
* a successful save without prop-drilling.
|
||||
*/
|
||||
export const useSettingsDialog = () => React.useContext(SettingsDialogContext)
|
||||
|
||||
type SettingsDialogProps = Omit<
|
||||
React.ComponentProps<typeof TabsPrimitive.Root>,
|
||||
"orientation"
|
||||
> & {
|
||||
onClose?: VoidFunction
|
||||
contentClassName?: string
|
||||
}
|
||||
|
||||
function SettingsDialog({
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
onClose,
|
||||
...props
|
||||
}: SettingsDialogProps) {
|
||||
const contextValue = React.useMemo(() => ({ onClose }), [onClose])
|
||||
|
||||
return (
|
||||
<DialogContent className={cn("min-w-5xl max-lg:min-w-[98vw] p-0 overflow-y-hidden", contentClassName)} showCloseButton={false}>
|
||||
<SettingsDialogContext.Provider value={contextValue}>
|
||||
<TabsPrimitive.Root
|
||||
data-slot="settings-dialog"
|
||||
orientation="vertical"
|
||||
className={cn(
|
||||
"flex h-[calc(100vh-8rem)] bg-surface-menu-bar",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TabsPrimitive.Root>
|
||||
</SettingsDialogContext.Provider>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsTabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="settings-tabs"
|
||||
className={cn(
|
||||
"flex flex-col w-56 bg-surface-menu-bar rounded-s-lg shrink-0 overflow-y-auto m-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type SettingsTabGroupProps = React.ComponentProps<"div"> & {
|
||||
header?: React.ReactNode
|
||||
}
|
||||
|
||||
function SettingsTabGroup({
|
||||
children,
|
||||
header,
|
||||
className,
|
||||
...props
|
||||
}: SettingsTabGroupProps) {
|
||||
return (
|
||||
<div data-slot="settings-tab-group" className={className} {...props}>
|
||||
{header && (
|
||||
<div className="h-7.5 px-2 py-[7px] my-[3px] flex cursor-default gap-1.5 text-xs font-medium text-ink-gray-5 transition-all duration-300 ease-in-out sticky top-0 z-10 bg-surface-menu-bar">
|
||||
<span>{header}</span>
|
||||
</div>
|
||||
)}
|
||||
<nav className="space-y-[3px] px-1">{children}</nav>
|
||||
<div className="mb-0.5 mt-[5px]"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type SettingsTabItemProps = React.ComponentProps<typeof TabsPrimitive.Trigger> & {
|
||||
icon?: React.ReactNode
|
||||
label: React.ReactNode
|
||||
}
|
||||
|
||||
function SettingsTabItem({
|
||||
icon,
|
||||
label,
|
||||
className,
|
||||
...props
|
||||
}: SettingsTabItemProps) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="settings-tab-item"
|
||||
className={cn(
|
||||
"flex h-7.5 cursor-pointer items-center rounded text-ink-gray-6 duration-300 ease-in-out focus:outline-none focus:transition-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-outline-gray-3 w-full",
|
||||
"hover:bg-surface-gray-3",
|
||||
"data-[state=active]:bg-surface-selected data-[state=active]:shadow-sm data-[state=active]:hover:bg-surface-selected",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between duration-300 ease-in-out px-2 py-[7px]">
|
||||
<div className="flex items-center truncate">
|
||||
{icon && (
|
||||
<div className="[&_svg:not([class*='size-'])]:size-4 text-ink-gray-6 [&_svg:not([class*='text-'])]:text-ink-gray-6">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"flex-1 shrink-0 truncate text-sm duration-300 ease-in-out w-auto opacity-100 text-ink-gray-6",
|
||||
icon && "ms-2"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TabsPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsPanels({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="settings-panels"
|
||||
className={cn(
|
||||
"flex flex-col flex-1 overflow-y-auto bg-surface-modal",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsPanel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="settings-panel"
|
||||
className={cn("flex flex-col h-full w-full text-ink-gray-8 py-8 px-6 gap-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
*
|
||||
* <SettingsPanelHeader actions={<><Button>Add</Button></>}>
|
||||
*
|
||||
* <SettingsPanelTitle>Settings</SettingsPanelTitle>
|
||||
* <SettingsPanelDescription>Settings description</SettingsPanelDescription>
|
||||
*
|
||||
* </SettingsPanelHeader>
|
||||
*/
|
||||
function SettingsPanelHeader({
|
||||
className,
|
||||
children,
|
||||
actions,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { actions?: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex justify-between items-start px-2 text-ink-gray-7", className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex item-center space-x-2 w-fit justify-end">
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsPanelTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("flex gap-2 text-xl font-semibold leading-none h-5", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsPanelDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-p-base text-ink-gray-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsPanelContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex-1 flex flex-col overflow-y-auto px-2", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
SettingsDialog,
|
||||
SettingsTabs,
|
||||
SettingsTabGroup,
|
||||
SettingsTabItem,
|
||||
SettingsPanels,
|
||||
SettingsPanel,
|
||||
SettingsPanelHeader,
|
||||
SettingsPanelTitle,
|
||||
SettingsPanelDescription,
|
||||
SettingsPanelContent
|
||||
}
|
||||
Reference in New Issue
Block a user