@pagamio/frontend-commons-lib 0.8.270 → 0.8.271

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.
@@ -8,6 +8,6 @@ type DatePickerProps = {
8
8
  value?: Date | null;
9
9
  onChange?: (date: Date) => void;
10
10
  };
11
- declare function DatePicker({ value, disabled, name, onChange }: Readonly<DatePickerProps>): import("react/jsx-runtime").JSX.Element;
11
+ declare function DatePicker({ value, disabled, name, placeholder, onChange }: Readonly<DatePickerProps>): import("react/jsx-runtime").JSX.Element;
12
12
  export default DatePicker;
13
13
  export type { DatePickerProps };
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Datepicker } from 'flowbite-react';
3
3
  import { useEffect, useState } from 'react';
4
- function DatePicker({ value, disabled, name, onChange }) {
4
+ function DatePicker({ value, disabled, name, placeholder, onChange }) {
5
5
  // Parse a date value into a valid Date or null
6
6
  const getValidDate = (date) => {
7
7
  if (date === undefined || date === null)
@@ -14,8 +14,8 @@ function DatePicker({ value, disabled, name, onChange }) {
14
14
  useEffect(() => {
15
15
  setSelectedDate(getValidDate(value));
16
16
  }, [value]);
17
- // Initialize defaultValue for the Datepicker component
18
- const datepickerDefaultValue = new Date();
17
+ // Only set defaultValue when a value is explicitly provided
18
+ const datepickerDefaultValue = selectedDate ?? undefined;
19
19
  const datepickerTheme = {
20
20
  root: {
21
21
  input: {
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import type { Field, SelectOption } from '../../../types';
3
+ interface SearchableMultiSelectInputProps {
4
+ field: Field;
5
+ error?: {
6
+ message?: string;
7
+ };
8
+ options?: Array<SelectOption | string>;
9
+ value?: string[];
10
+ defaultValue?: string[];
11
+ onChange?: (selected: string[]) => void;
12
+ onBlur?: () => void;
13
+ name?: string;
14
+ }
15
+ declare const SearchableMultiSelectInput: React.ForwardRefExoticComponent<SearchableMultiSelectInputProps & React.RefAttributes<HTMLDivElement>>;
16
+ export default SearchableMultiSelectInput;
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Cross2Icon } from '@radix-ui/react-icons';
3
+ import { ChevronDownIcon } from '@radix-ui/react-icons';
4
+ import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
5
+ import Button from '../../../../components/ui/Button';
6
+ import { cn } from '../../../../helpers';
7
+ const normalizeOptions = (options) => {
8
+ if (!options)
9
+ return [];
10
+ return options.map((opt) => (typeof opt === 'string' ? { label: opt, value: opt } : opt));
11
+ };
12
+ const SearchableMultiSelectInput = forwardRef(({ field, error, options: rawOptions, value, defaultValue = [], onChange, onBlur }, ref) => {
13
+ const options = normalizeOptions(rawOptions);
14
+ const [selectedValues, setSelectedValues] = useState(defaultValue);
15
+ const [isOpen, setIsOpen] = useState(false);
16
+ const [searchQuery, setSearchQuery] = useState('');
17
+ const [dropdownPos, setDropdownPos] = useState(null);
18
+ const containerRef = useRef(null);
19
+ const triggerRef = useRef(null);
20
+ const searchInputRef = useRef(null);
21
+ useEffect(() => {
22
+ if (value !== undefined) {
23
+ setSelectedValues(value);
24
+ }
25
+ }, [value]);
26
+ const updateDropdownPosition = useCallback(() => {
27
+ if (!triggerRef.current)
28
+ return;
29
+ const rect = triggerRef.current.getBoundingClientRect();
30
+ setDropdownPos({
31
+ top: rect.bottom + window.scrollY + 4,
32
+ left: rect.left + window.scrollX,
33
+ width: rect.width,
34
+ });
35
+ }, []);
36
+ useEffect(() => {
37
+ if (!isOpen)
38
+ return;
39
+ updateDropdownPosition();
40
+ window.addEventListener('scroll', updateDropdownPosition, true);
41
+ window.addEventListener('resize', updateDropdownPosition);
42
+ return () => {
43
+ window.removeEventListener('scroll', updateDropdownPosition, true);
44
+ window.removeEventListener('resize', updateDropdownPosition);
45
+ };
46
+ }, [isOpen, updateDropdownPosition]);
47
+ useEffect(() => {
48
+ if (!isOpen)
49
+ return;
50
+ const handleOutside = (e) => {
51
+ const target = e.target;
52
+ const clickedInsideTrigger = containerRef.current?.contains(target);
53
+ const dropdown = document.getElementById(`sms-dropdown-${field.name}`);
54
+ const clickedInsideDropdown = dropdown?.contains(target);
55
+ if (!clickedInsideTrigger && !clickedInsideDropdown) {
56
+ setIsOpen(false);
57
+ setSearchQuery('');
58
+ onBlur?.();
59
+ }
60
+ };
61
+ document.addEventListener('mousedown', handleOutside);
62
+ return () => document.removeEventListener('mousedown', handleOutside);
63
+ }, [isOpen, field.name, onBlur]);
64
+ useEffect(() => {
65
+ if (isOpen && searchInputRef.current) {
66
+ searchInputRef.current.focus();
67
+ }
68
+ }, [isOpen]);
69
+ const internalValues = value ?? selectedValues;
70
+ const handleToggle = (optionValue) => {
71
+ const updated = internalValues.includes(optionValue)
72
+ ? internalValues.filter((v) => v !== optionValue)
73
+ : [...internalValues, optionValue];
74
+ if (value === undefined)
75
+ setSelectedValues(updated);
76
+ onChange?.(updated);
77
+ };
78
+ const removeTag = (optionValue, e) => {
79
+ e.stopPropagation();
80
+ handleToggle(optionValue);
81
+ };
82
+ const handleTriggerClick = () => {
83
+ if (field.disabled)
84
+ return;
85
+ if (!isOpen)
86
+ updateDropdownPosition();
87
+ setIsOpen((o) => !o);
88
+ };
89
+ const filteredOptions = options.filter((opt) => opt.label.toLowerCase().includes(searchQuery.toLowerCase()));
90
+ const selectedOptions = options.filter((o) => internalValues.includes(o.value));
91
+ const displayPlaceholder = field.placeholder ?? 'Search and select...';
92
+ return (_jsxs(_Fragment, { children: [_jsx("label", { htmlFor: field.name, className: "block text-sm font-medium text-foreground", children: field.label }), _jsxs("div", { ref: containerRef, className: "relative w-full", children: [_jsxs("button", { ref: triggerRef, type: "button", disabled: field.disabled, onClick: handleTriggerClick, onKeyDown: (e) => {
93
+ if (e.key === 'Enter' || e.key === ' ') {
94
+ e.preventDefault();
95
+ handleTriggerClick();
96
+ }
97
+ if (e.key === 'Escape') {
98
+ setIsOpen(false);
99
+ setSearchQuery('');
100
+ }
101
+ }, className: cn('flex items-center w-full px-3 py-2 text-sm border border-input rounded-md bg-background text-left', 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0', 'transition-colors duration-150 min-h-[38px]', field.disabled
102
+ ? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
103
+ : 'cursor-pointer hover:border-primary/60', isOpen && 'border-primary ring-2 ring-ring'), "aria-haspopup": "listbox", "aria-expanded": isOpen, children: [_jsx("span", { className: "flex-1 min-w-0", children: _jsx("span", { className: cn('truncate block', selectedOptions.length === 0 && 'text-muted-foreground'), children: selectedOptions.length === 0 ? displayPlaceholder : `${selectedOptions.length} selected` }) }), _jsx(ChevronDownIcon, { className: cn('ml-2 flex-shrink-0 text-muted-foreground transition-transform duration-150', isOpen && 'rotate-180') })] }), selectedOptions.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1 mt-2", children: selectedOptions.map((opt) => (_jsxs("span", { className: "inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-primary/10 text-primary", children: [opt.label, _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": `Remove ${opt.label}`, onClick: (e) => removeTag(opt.value, e), className: "h-4 w-4 p-0 hover:bg-primary/20 rounded-full", children: _jsx(Cross2Icon, { className: "w-2.5 h-2.5" }) })] }, opt.value))) })), isOpen && dropdownPos && (_jsxs("div", { ref: ref, id: `sms-dropdown-${field.name}`, style: {
104
+ position: 'fixed',
105
+ top: dropdownPos.top,
106
+ left: dropdownPos.left,
107
+ width: dropdownPos.width,
108
+ }, className: cn('z-[9999]', 'bg-popover border border-border rounded-md shadow-lg', 'animate-in fade-in-0 zoom-in-95 duration-100'), children: [_jsx("div", { className: "p-2 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", className: "w-full px-2 py-1.5 text-sm border border-input rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-ring" }) }), _jsx("ul", { className: "max-h-56 overflow-y-auto p-1.5 space-y-0.5", children: filteredOptions.length > 0 ? (filteredOptions.map((option) => {
109
+ const checked = internalValues.includes(option.value);
110
+ const id = `sms-opt-${field.name}-${option.value}`;
111
+ return (_jsx("li", { children: _jsxs("label", { htmlFor: id, className: cn('flex items-center gap-2.5 px-2 py-2 rounded-md cursor-pointer text-sm', 'hover:bg-accent hover:text-accent-foreground transition-colors', checked && 'bg-primary/5'), children: [_jsx("input", { id: id, type: "checkbox", checked: checked, onChange: () => handleToggle(option.value), className: cn('w-4 h-4 rounded border border-input bg-background flex-shrink-0', 'accent-[hsl(var(--primary))] cursor-pointer', 'focus:ring-2 focus:ring-ring focus:ring-offset-0') }), _jsx("span", { className: "flex-1 font-medium text-foreground", children: option.label })] }) }, option.value));
112
+ })) : (_jsx("li", { className: "px-3 py-2 text-sm text-muted-foreground text-center", children: "No options found" })) })] }))] }), error && _jsx("p", { className: "mt-2 text-sm text-red-500", children: error.message })] }));
113
+ });
114
+ SearchableMultiSelectInput.displayName = 'SearchableMultiSelectInput';
115
+ export default SearchableMultiSelectInput;
@@ -31,7 +31,7 @@ export const setupInputRegistry = async () => {
31
31
  return getRegistryPromise();
32
32
  };
33
33
  const doSetup = async () => {
34
- const [{ default: CardExpiryInput }, { default: CheckboxInput }, { default: ColorInput }, { default: CreditCardInput }, { default: DateInput }, { default: EmailInput }, { default: LabelInput }, { default: MultiSelectInputComponent }, { default: NumberInput }, { default: PasswordInput }, { default: RadioInput }, { default: SelectInput }, { default: TextareaInputFW }, { default: TimeInput }, { default: ToggleSwitchInput }, { default: UploadFieldForm }, { default: PhoneInput },] = await Promise.all([
34
+ const [{ default: CardExpiryInput }, { default: CheckboxInput }, { default: ColorInput }, { default: CreditCardInput }, { default: DateInput }, { default: EmailInput }, { default: LabelInput }, { default: MultiSelectInputComponent }, { default: NumberInput }, { default: PasswordInput }, { default: RadioInput }, { default: SelectInput }, { default: TextareaInputFW }, { default: TimeInput }, { default: ToggleSwitchInput }, { default: UploadFieldForm }, { default: PhoneInput }, { default: SearchableMultiSelectInput },] = await Promise.all([
35
35
  import('../components/inputs/card-expiry-input/CardExpiryInput'),
36
36
  import('../components/inputs/checkbox-input/CheckboxInput'),
37
37
  import('../components/inputs/color-input/ColorInput'),
@@ -49,6 +49,7 @@ const doSetup = async () => {
49
49
  import('../components/inputs/toggle-switch-input/ToggleSwitchInput'),
50
50
  import('../components/inputs/upload-field/UploadFieldForm'),
51
51
  import('../../components/ui/PhoneInput'),
52
+ import('../components/inputs/searchable-multi-select/SearchableMultiSelectInput'),
52
53
  ]);
53
54
  registerInput('card-expiry-input', CardExpiryInput);
54
55
  registerInput('checkbox', CheckboxInput);
@@ -67,5 +68,6 @@ const doSetup = async () => {
67
68
  registerInput('text', LabelInput);
68
69
  registerInput('textarea', TextareaInputFW);
69
70
  registerInput('time', TimeInput);
71
+ registerInput('searchable-multi-select', SearchableMultiSelectInput);
70
72
  registryDone = true;
71
73
  };
@@ -2,10 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useForm } from 'react-hook-form';
3
3
  import { useEffect, useRef, useState } from 'react';
4
4
  import { Button, NotificationModal } from '../../components';
5
+ import { useToast } from '../../context';
5
6
  import { FieldWrapper } from '../../form-engine';
6
7
  import { useFormPersistence } from '../../form-engine/hooks/useFormPersistence';
7
8
  const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonText = 'Submit', cancelButtonText = 'Cancel', showForm = true, showDrawerButtons = true, handleCloseDrawer, onFieldUpdate, onFieldChange, children, updateFieldOptions, updateFieldLabel, setFieldHidden, addField, updateFieldValidation, processInitialFieldUpdates, processingDependencyRef, pendingUpdatesRef, persistenceKey, }) => {
8
9
  const { saveFormData, restoreFormData, clearPersistedData, hasPersistedData } = useFormPersistence(persistenceKey);
10
+ const { addToast } = useToast();
9
11
  // Determine initial values: initialValues take precedence over persisted data
10
12
  // This ensures that when editing different items, the form shows the correct item's data
11
13
  const getEffectiveInitialValues = () => {
@@ -24,7 +26,6 @@ const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonTe
24
26
  };
25
27
  const { control, handleSubmit, watch, setValue, clearErrors, formState: { errors, isSubmitting }, reset, getValues, } = useForm({ mode: 'onBlur', defaultValues: getEffectiveInitialValues() });
26
28
  const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
27
- const [submitError, setSubmitError] = useState(null);
28
29
  const [showCancelConfirmModal, setShowCancelConfirmModal] = useState(false);
29
30
  const fieldChangeRef = useRef(new Set());
30
31
  const initialLoadPerformedRef = useRef(false);
@@ -74,7 +75,6 @@ const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonTe
74
75
  }
75
76
  }, [isOpen, processInitialFieldUpdates]);
76
77
  const handleFormSubmit = async (data) => {
77
- setSubmitError(null);
78
78
  try {
79
79
  const visibleFieldsData = Object.fromEntries(Object.entries(data).filter(([key]) => !fields.find((field) => field.name === key && field.isHidden)));
80
80
  await onSubmit(visibleFieldsData);
@@ -86,19 +86,16 @@ const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonTe
86
86
  handleCloseDrawer();
87
87
  }
88
88
  catch (error) {
89
- setSubmitError(error instanceof Error ? error.message : 'An error occurred');
89
+ const message = error instanceof Error ? error.message : 'An error occurred';
90
+ addToast({ title: 'Error', message, variant: 'error' });
90
91
  }
91
92
  };
92
- // Reset form when drawer closes
93
+ // Reset form when drawer closes (persisted data is preserved for draft resumption)
93
94
  useEffect(() => {
94
95
  if (!isOpen) {
95
96
  reset();
96
- // Clear persisted data when drawer closes to prevent stale data on next open
97
- if (persistenceKey) {
98
- clearPersistedData();
99
- }
100
97
  }
101
- }, [isOpen, reset, persistenceKey, clearPersistedData]);
98
+ }, [isOpen, reset]);
102
99
  // Reset form when initialValues change, but preserve current form values
103
100
  useEffect(() => {
104
101
  if (isOpen && initialValues) {
@@ -233,7 +230,7 @@ const DrawerContent = ({ fields, onSubmit, isOpen, initialValues, submitButtonTe
233
230
  return (_jsxs(_Fragment, { children: [showForm && (_jsxs("form", { className: "grid grid-cols-12 gap-2 align-middle mb-6 border-input px-6 py-5", onSubmit: handleSubmit(handleFormSubmit), children: [fields?.map((field) => (_jsx(FieldWrapper, { field: {
234
231
  ...field,
235
232
  validation: processDependencyValidation(field),
236
- }, control: control, errors: errors, layout: "vertical", onFieldUpdate: onFieldUpdate, onFieldChange: onFieldChange, updateFieldOptions: updateFieldOptions, updateFieldLabel: updateFieldLabel, setFieldHidden: setFieldHidden, addField: addField, setValue: setValue, clearErrors: clearErrors, getValues: getValues, pendingUpdatesRef: pendingUpdatesRef, notifyPendingUpdate: () => setPendingUpdateCount((prev) => prev + 1), processingDependencyRef: processingDependencyRef, fieldChangeRef: fieldChangeRef }, field.name))), isSubmitting && _jsx("span", { children: "Submitting..." }), submitError && _jsx("p", { className: "text-red-500", children: submitError })] })), children, showDrawerButtons && (_jsxs("div", { className: "flex border-t bg-background", style: {
233
+ }, control: control, errors: errors, layout: "vertical", onFieldUpdate: onFieldUpdate, onFieldChange: onFieldChange, updateFieldOptions: updateFieldOptions, updateFieldLabel: updateFieldLabel, setFieldHidden: setFieldHidden, addField: addField, setValue: setValue, clearErrors: clearErrors, getValues: getValues, pendingUpdatesRef: pendingUpdatesRef, notifyPendingUpdate: () => setPendingUpdateCount((prev) => prev + 1), processingDependencyRef: processingDependencyRef, fieldChangeRef: fieldChangeRef }, field.name))), isSubmitting && _jsx("span", { children: "Submitting..." })] })), children, showDrawerButtons && (_jsxs("div", { className: "flex border-t bg-background", style: {
237
234
  height: '50px',
238
235
  position: 'fixed',
239
236
  bottom: 0,
@@ -130,6 +130,6 @@ const FormEngineDrawer = ({ title, cancelButtonText = 'Cancel', submitButtonText
130
130
  processingDependencyRef.current = false;
131
131
  }
132
132
  };
133
- return (_jsx(BaseDrawer, { title: title, isOpen: isOpen, marginTop: marginTop, onClose: onClose, children: _jsx(DrawerContent, { isOpen: isOpen, fields: fields, onSubmit: onSubmit, handleCloseDrawer: onClose, initialValues: initialValues, showForm: showForm, showDrawerButtons: showDrawerButtons, cancelButtonText: cancelButtonText, submitButtonText: submitButtonText, onFieldUpdate: onFieldUpdate, onFieldChange: onFieldChange, updateFieldOptions: updateFieldOptions, updateFieldLabel: updateFieldLabel, setFieldHidden: setFieldHidden, addField: addField, updateFieldValidation: updateFieldValidation, processInitialFieldUpdates: processInitialFieldUpdates, processingDependencyRef: processingDependencyRef, pendingUpdatesRef: pendingUpdatesRef, persistenceKey: persistenceKey, children: children }, `${persistenceKey}-${isOpen ? 'open' : 'closed'}`) }));
133
+ return (_jsx(BaseDrawer, { title: title, isOpen: isOpen, marginTop: marginTop, onClose: onClose, children: _jsx(DrawerContent, { isOpen: isOpen, fields: fields, onSubmit: onSubmit, handleCloseDrawer: onClose, initialValues: initialValues, showForm: showForm, showDrawerButtons: showDrawerButtons, cancelButtonText: cancelButtonText, submitButtonText: submitButtonText, onFieldUpdate: onFieldUpdate, onFieldChange: onFieldChange, updateFieldOptions: updateFieldOptions, updateFieldLabel: updateFieldLabel, setFieldHidden: setFieldHidden, addField: addField, updateFieldValidation: updateFieldValidation, processInitialFieldUpdates: processInitialFieldUpdates, processingDependencyRef: processingDependencyRef, pendingUpdatesRef: pendingUpdatesRef, persistenceKey: persistenceKey, children: children }, persistenceKey || 'drawer-content') }));
134
134
  };
135
135
  export default FormEngineDrawer;
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.270",
4
+ "version": "0.8.271",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false