@pagamio/frontend-commons-lib 0.8.351 → 0.8.353
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/lib/components/ui/PagamioSearchableCombobox.d.ts +21 -1
- package/lib/components/ui/PagamioSearchableCombobox.js +15 -8
- package/lib/components/ui/TableDownload.js +1 -1
- package/lib/form-engine/FormEngine.js +32 -26
- package/lib/form-engine/components/inputs/searchable-combobox/SearchableComboboxInput.js +1 -1
- package/lib/form-engine/types/index.d.ts +11 -0
- package/lib/pagamio-table/data-table/TableDownload.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { UsePagamioComboboxReturn } from '../../shared/hooks/usePagamioCombobox';
|
|
3
|
+
/**
|
|
4
|
+
* Optional in-popover action — replaces the old "+ Create New X" sentinel
|
|
5
|
+
* option pattern that some `<Select>` callers used to inline a "create a
|
|
6
|
+
* new item" affordance. Rendered as a button row pinned to the top of the
|
|
7
|
+
* popover, above the options list. Clicking it closes the popover and
|
|
8
|
+
* fires `onClick` (typically opening a quick-create modal). The handler
|
|
9
|
+
* is responsible for the side effect; the combobox does NOT update its
|
|
10
|
+
* `value` automatically.
|
|
11
|
+
*/
|
|
12
|
+
export interface SearchableComboboxActionButton {
|
|
13
|
+
/** Label shown on the action button (e.g. "+ New category"). */
|
|
14
|
+
label: React.ReactNode;
|
|
15
|
+
onClick: () => void;
|
|
16
|
+
}
|
|
3
17
|
export interface PagamioSearchableComboboxProps<T extends {
|
|
4
18
|
id: string;
|
|
5
19
|
}> {
|
|
@@ -8,6 +22,12 @@ export interface PagamioSearchableComboboxProps<T extends {
|
|
|
8
22
|
/** Controlled selected id, or null when nothing is selected. */
|
|
9
23
|
value: string | null;
|
|
10
24
|
onChange: (id: string | null) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Optional action button rendered at the top of the popover. Used to
|
|
27
|
+
* surface a "+ Create new …" affordance inside the picker itself,
|
|
28
|
+
* matching where the old static-select sentinel option lived.
|
|
29
|
+
*/
|
|
30
|
+
actionButton?: SearchableComboboxActionButton;
|
|
11
31
|
/**
|
|
12
32
|
* Optional pre-resolved selected option. When set, the trigger shows its
|
|
13
33
|
* label without needing to fetch via `restoreIds`. If both this and the
|
|
@@ -39,4 +59,4 @@ export interface PagamioSearchableComboboxProps<T extends {
|
|
|
39
59
|
}
|
|
40
60
|
export declare function PagamioSearchableCombobox<T extends {
|
|
41
61
|
id: string;
|
|
42
|
-
}>({ combobox, value, onChange, selectedOption, placeholder, searchPlaceholder, emptyMessage, cappedMessage, loadingMessage, disabled, required, name, getLabel, renderOption, renderSelected, className, triggerClassName, contentClassName, }: PagamioSearchableComboboxProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
62
|
+
}>({ combobox, value, onChange, actionButton, selectedOption, placeholder, searchPlaceholder, emptyMessage, cappedMessage, loadingMessage, disabled, required, name, getLabel, renderOption, renderSelected, className, triggerClassName, contentClassName, }: PagamioSearchableComboboxProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -19,7 +19,7 @@ function defaultGetLabel(option) {
|
|
|
19
19
|
const o = option;
|
|
20
20
|
return o.name ?? option.id;
|
|
21
21
|
}
|
|
22
|
-
export function PagamioSearchableCombobox({ combobox, value, onChange, selectedOption, placeholder = 'Select…', searchPlaceholder = 'Search...', emptyMessage = 'No options found', cappedMessage = 'Showing top results — refine your search.', loadingMessage = 'Loading…', disabled, required, name, getLabel = defaultGetLabel, renderOption, renderSelected, className, triggerClassName, contentClassName, }) {
|
|
22
|
+
export function PagamioSearchableCombobox({ combobox, value, onChange, actionButton, selectedOption, placeholder = 'Select…', searchPlaceholder = 'Search...', emptyMessage = 'No options found', cappedMessage = 'Showing top results — refine your search.', loadingMessage = 'Loading…', disabled, required, name, getLabel = defaultGetLabel, renderOption, renderSelected, className, triggerClassName, contentClassName, }) {
|
|
23
23
|
const [open, setOpen] = React.useState(false);
|
|
24
24
|
const inputRef = React.useRef(null);
|
|
25
25
|
// Focus the search input when the popover opens — matches the Select's
|
|
@@ -52,11 +52,18 @@ export function PagamioSearchableCombobox({ combobox, value, onChange, selectedO
|
|
|
52
52
|
: placeholder }), _jsx(ChevronDownIcon, { className: "ml-2 h-4 w-4 opacity-50 shrink-0" })] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 4,
|
|
53
53
|
// Mirrors SelectContent: same shadow + animation classes, popover
|
|
54
54
|
// background, popover-foreground text. Width matches trigger.
|
|
55
|
-
className: cn('p-0 w-[var(--radix-popover-trigger-width)] min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md', contentClassName), children: [_jsx("div", { className: "p-2 border-b border-border", children: _jsx("input", { ref: inputRef, type: "text", placeholder: searchPlaceholder, value: combobox.search, onChange: (e) => combobox.setSearch(e.target.value), onKeyDown: (e) => e.stopPropagation(), className: "w-full px-2 py-1 text-sm border border-input rounded bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" }) }),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
className: cn('p-0 w-[var(--radix-popover-trigger-width)] min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md', contentClassName), children: [_jsx("div", { className: "p-2 border-b border-border", children: _jsx("input", { ref: inputRef, type: "text", placeholder: searchPlaceholder, value: combobox.search, onChange: (e) => combobox.setSearch(e.target.value), onKeyDown: (e) => e.stopPropagation(), className: "w-full px-2 py-1 text-sm border border-input rounded bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" }) }), _jsxs("div", { className: "p-1 max-h-[200px] overflow-y-auto", children: [actionButton ? (_jsx("button", { type: "button", onClick: () => {
|
|
56
|
+
setOpen(false);
|
|
57
|
+
actionButton.onClick();
|
|
58
|
+
}, className: cn(
|
|
59
|
+
// Same row geometry as an option row so the picker reads
|
|
60
|
+
// as one consistent list. Primary-coloured label keeps it
|
|
61
|
+
// visually distinct from real options.
|
|
62
|
+
'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none text-left font-medium text-primary', 'hover:bg-muted focus:bg-muted'), children: actionButton.label }, "__action__")) : null, combobox.isLoading && combobox.options.length === 0 ? (_jsx("div", { className: "px-2 py-1.5 text-sm text-muted-foreground", children: loadingMessage })) : combobox.error ? (_jsx("div", { className: "px-2 py-1.5 text-sm text-destructive", children: combobox.error.message || 'Failed to load options.' })) : combobox.options.length === 0 ? (_jsx("div", { className: "px-2 py-1.5 text-sm text-muted-foreground", children: emptyMessage })) : (_jsxs(_Fragment, { children: [combobox.options.map((option) => {
|
|
63
|
+
const isSelected = option.id === value;
|
|
64
|
+
return (_jsx("button", { type: "button", role: "option", "aria-selected": isSelected, onClick: () => handleSelect(option.id), className: cn(
|
|
65
|
+
// Mirrors SelectItem styling: padding, hover via
|
|
66
|
+
// focus:bg-muted, selected via bg-accent.
|
|
67
|
+
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none text-left', 'hover:bg-muted focus:bg-muted focus:text-foreground', isSelected && 'bg-accent text-foreground'), children: renderOption ? (renderOption(option, isSelected)) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "line-clamp-1", children: getLabel(option) }), isSelected ? (_jsx("span", { className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(CheckIcon, { className: "h-4 w-4" }) })) : null] })) }, option.id));
|
|
68
|
+
}), combobox.isCapped ? (_jsx("div", { className: "mt-1 border-t px-2 py-1.5 text-xs text-muted-foreground", children: cappedMessage })) : null] }))] })] })] }), name ? _jsx("input", { type: "hidden", name: name, value: value ?? '' }) : null] }));
|
|
62
69
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
3
|
import { Dropdown, DropdownItem } from 'flowbite-react';
|
|
4
4
|
import { jsPDF } from 'jspdf';
|
|
5
|
-
import
|
|
5
|
+
import autoTable from 'jspdf-autotable';
|
|
6
6
|
import { FiDownload } from 'react-icons/fi';
|
|
7
7
|
import * as XLSX from 'xlsx';
|
|
8
8
|
const TableDownload = ({ columns, data }) => {
|
|
@@ -47,31 +47,36 @@ const FormEngine = ({ fields, onSubmit, initialValues, layout = 'vertical', isNo
|
|
|
47
47
|
}
|
|
48
48
|
return initialValues;
|
|
49
49
|
};
|
|
50
|
-
const { control, handleSubmit, watch, formState: { errors, isSubmitting }, reset, setValue, trigger, } = useForm({ mode: 'onBlur', defaultValues: getEffectiveInitialValues() });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
//
|
|
50
|
+
const { control, handleSubmit, watch, formState: { errors, isSubmitting }, reset, setValue, trigger, getValues, } = useForm({ mode: 'onBlur', defaultValues: getEffectiveInitialValues() });
|
|
51
|
+
// Subscribe to field changes via the callback form of `watch` so this
|
|
52
|
+
// component does NOT re-render on every keystroke / select change. The
|
|
53
|
+
// previous `const allFields = watch()` (no args) caused the entire
|
|
54
|
+
// FormEngine subtree — including all Radix Select popovers — to re-render
|
|
55
|
+
// synchronously inside the field's onChange. That race produced the
|
|
56
|
+
// "click twice to commit" symptom on Select inputs: the first click's
|
|
57
|
+
// outside-dismiss fired during the parent re-render and the popover lost
|
|
58
|
+
// its open state. Subscribing via callback keeps RHF state updates
|
|
59
|
+
// isolated to the affected Controller subtree.
|
|
55
60
|
useEffect(() => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
getFieldValues(allFields);
|
|
63
|
-
}
|
|
64
|
-
}, [allFields, getFieldValues]);
|
|
65
|
-
// Save form data when fields change (for persistence)
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (persistenceKey && allFields && Object.keys(allFields).length > 0) {
|
|
68
|
-
// Only save if there are actual field values (not just empty object)
|
|
69
|
-
const hasValues = Object.values(allFields).some((value) => value !== undefined && value !== null && value !== '');
|
|
70
|
-
if (hasValues) {
|
|
71
|
-
saveFormData(allFields, true);
|
|
61
|
+
const subscription = watch((values, info) => {
|
|
62
|
+
if (info?.name === 'password') {
|
|
63
|
+
const cp = values?.confirmPassword;
|
|
64
|
+
if (values?.password && cp) {
|
|
65
|
+
trigger('confirmPassword');
|
|
66
|
+
}
|
|
72
67
|
}
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
if (getFieldValues) {
|
|
69
|
+
getFieldValues(values);
|
|
70
|
+
}
|
|
71
|
+
if (persistenceKey) {
|
|
72
|
+
const hasValues = Object.values(values ?? {}).some((value) => value !== undefined && value !== null && value !== '');
|
|
73
|
+
if (hasValues) {
|
|
74
|
+
saveFormData(values, true);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return () => subscription.unsubscribe();
|
|
79
|
+
}, [watch, trigger, getFieldValues, persistenceKey, saveFormData]);
|
|
75
80
|
// Expose form control methods via ref
|
|
76
81
|
useEffect(() => {
|
|
77
82
|
if (formRef && 'current' in formRef) {
|
|
@@ -109,11 +114,12 @@ const FormEngine = ({ fields, onSubmit, initialValues, layout = 'vertical', isNo
|
|
|
109
114
|
const validateConfirmPassword = (value) => {
|
|
110
115
|
if (!value)
|
|
111
116
|
return 'Please confirm your password';
|
|
112
|
-
|
|
117
|
+
const currentPassword = getValues('password');
|
|
118
|
+
if (!currentPassword)
|
|
113
119
|
return 'Please enter a password first';
|
|
114
|
-
if (
|
|
120
|
+
if (currentPassword.length < 8)
|
|
115
121
|
return 'Please enter a valid password first (minimum 8 characters)';
|
|
116
|
-
if (value !==
|
|
122
|
+
if (value !== currentPassword)
|
|
117
123
|
return 'Passwords do not match';
|
|
118
124
|
return true;
|
|
119
125
|
};
|
|
@@ -34,7 +34,7 @@ const SearchableComboboxInput = forwardRef(({ field, value, onChange }, ref) =>
|
|
|
34
34
|
debounceMs: source.debounceMs,
|
|
35
35
|
restoreIds,
|
|
36
36
|
});
|
|
37
|
-
return (_jsxs("div", { ref: ref, children: [field.label && (_jsx("label", { htmlFor: field.name, className: "block text-sm font-medium text-foreground mb-1", children: field.label })), _jsx(PagamioSearchableCombobox, { combobox: combobox, value: value ?? null, onChange: (id) => onChange?.(id), placeholder: field.placeholder, disabled: field.disabled, required: field.validation?.required ? true : undefined, name: field.name, getLabel: source.getLabel })] }));
|
|
37
|
+
return (_jsxs("div", { ref: ref, children: [field.label && (_jsx("label", { htmlFor: field.name, className: "block text-sm font-medium text-foreground mb-1", children: field.label })), _jsx(PagamioSearchableCombobox, { combobox: combobox, value: value ?? null, onChange: (id) => onChange?.(id), actionButton: field.actionButton, placeholder: field.placeholder, disabled: field.disabled, required: field.validation?.required ? true : undefined, name: field.name, getLabel: source.getLabel })] }));
|
|
38
38
|
});
|
|
39
39
|
SearchableComboboxInput.displayName = 'SearchableComboboxInput';
|
|
40
40
|
export default SearchableComboboxInput;
|
|
@@ -215,6 +215,17 @@ export interface Field {
|
|
|
215
215
|
* Tells the form engine how to fetch lookup results.
|
|
216
216
|
*/
|
|
217
217
|
source?: ComboboxSourceSpec;
|
|
218
|
+
/**
|
|
219
|
+
* Optional in-picker action for `searchable-combobox`. Renders as the
|
|
220
|
+
* first row of the popover (above the options) — the same place a
|
|
221
|
+
* "+ Create new …" sentinel option used to live in the old static
|
|
222
|
+
* `<Select>`. Clicking it closes the popover and fires `onClick`; the
|
|
223
|
+
* handler owns the side effect (e.g. opens a quick-create modal).
|
|
224
|
+
*/
|
|
225
|
+
actionButton?: {
|
|
226
|
+
label: string;
|
|
227
|
+
onClick: () => void;
|
|
228
|
+
};
|
|
218
229
|
}
|
|
219
230
|
export interface DependentFieldUpdate {
|
|
220
231
|
field: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Dropdown, DropdownItem } from 'flowbite-react';
|
|
3
3
|
import { jsPDF } from 'jspdf';
|
|
4
|
-
import
|
|
4
|
+
import autoTable from 'jspdf-autotable';
|
|
5
5
|
import { FiDownload } from 'react-icons/fi';
|
|
6
6
|
import * as XLSX from 'xlsx';
|
|
7
7
|
const TableDownload = ({ columns, data }) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagamio/frontend-commons-lib",
|
|
3
3
|
"description": "Pagamio library for Frontend reusable components like the form engine and table container",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.353",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|