proje-react-panel 1.0.15 → 1.0.16
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/.vscode/launch.json +10 -0
 - package/dist/components/components/FormField.d.ts +2 -1
 - package/dist/components/components/InnerForm.d.ts +2 -2
 - package/dist/components/components/list/Datagrid.d.ts +13 -0
 - package/dist/components/components/list/EmptyList.d.ts +2 -0
 - package/dist/components/components/list/FilterPopup.d.ts +11 -0
 - package/dist/components/components/list/ListPage.d.ts +22 -0
 - package/dist/components/components/list/Pagination.d.ts +11 -0
 - package/dist/components/components/list/index.d.ts +0 -0
 - package/dist/components/list/Datagrid.d.ts +8 -4
 - package/dist/components/list/EmptyList.d.ts +2 -0
 - package/dist/components/list/FilterPopup.d.ts +10 -0
 - package/dist/components/pages/FormPage.d.ts +3 -2
 - package/dist/components/pages/ListPage.d.ts +2 -1
 - package/dist/decorators/form/Input.d.ts +7 -3
 - package/dist/decorators/list/Cell.d.ts +14 -2
 - package/dist/decorators/list/List.d.ts +24 -1
 - package/dist/index.cjs.js +1 -1
 - package/dist/index.d.ts +4 -3
 - package/dist/index.esm.js +1 -1
 - package/package.json +9 -3
 - package/src/assets/icons/svg/create.svg +9 -0
 - package/src/assets/icons/svg/filter.svg +3 -0
 - package/src/assets/icons/svg/pencil.svg +8 -0
 - package/src/assets/icons/svg/search.svg +8 -0
 - package/src/assets/icons/svg/trash.svg +8 -0
 - package/src/components/components/FormField.tsx +41 -7
 - package/src/components/components/InnerForm.tsx +8 -9
 - package/src/components/components/list/Datagrid.tsx +121 -0
 - package/src/components/components/list/EmptyList.tsx +26 -0
 - package/src/components/components/list/FilterPopup.tsx +202 -0
 - package/src/components/components/list/ListPage.tsx +178 -0
 - package/src/components/pages/FormPage.tsx +4 -2
 - package/src/decorators/form/Input.ts +4 -3
 - package/src/decorators/list/Cell.ts +24 -14
 - package/src/decorators/list/List.ts +23 -9
 - package/src/index.ts +8 -3
 - package/src/styles/filter-popup.scss +134 -0
 - package/src/styles/index.scss +18 -22
 - package/src/styles/list.scss +149 -8
 - package/src/types/svg.d.ts +5 -0
 - package/src/components/list/Datagrid.tsx +0 -101
 - package/src/components/pages/ListPage.tsx +0 -85
 - /package/src/components/{list → components/list}/Pagination.tsx +0 -0
 - /package/src/components/{list → components/list}/index.ts +0 -0
 - /package/src/styles/{_scrollbar.scss → utils/scrollbar.scss} +0 -0
 
| 
         @@ -0,0 +1,121 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import React from 'react';
         
     | 
| 
      
 2 
     | 
    
         
            +
            import { CellOptions } from '../../../decorators/list/Cell';
         
     | 
| 
      
 3 
     | 
    
         
            +
            import { Link } from 'react-router';
         
     | 
| 
      
 4 
     | 
    
         
            +
            import { useAppStore } from '../../../store/store';
         
     | 
| 
      
 5 
     | 
    
         
            +
            import { ImageCellOptions } from '../../../decorators/list/ImageCell';
         
     | 
| 
      
 6 
     | 
    
         
            +
            import { ListData } from '../../../decorators/list/ListData';
         
     | 
| 
      
 7 
     | 
    
         
            +
            import { EmptyList } from './EmptyList';
         
     | 
| 
      
 8 
     | 
    
         
            +
            import SearchIcon from '../../../assets/icons/svg/search.svg';
         
     | 
| 
      
 9 
     | 
    
         
            +
            import PencilIcon from '../../../assets/icons/svg/pencil.svg';
         
     | 
| 
      
 10 
     | 
    
         
            +
            import TrashIcon from '../../../assets/icons/svg/trash.svg';
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            interface DatagridProps<T extends { id: string }> {
         
     | 
| 
      
 13 
     | 
    
         
            +
              data: T[];
         
     | 
| 
      
 14 
     | 
    
         
            +
              listData: ListData;
         
     | 
| 
      
 15 
     | 
    
         
            +
              onRemoveItem?: (item: T) => Promise<void>;
         
     | 
| 
      
 16 
     | 
    
         
            +
            }
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            export function Datagrid<T extends { id: string }>({ data, listData, onRemoveItem }: DatagridProps<T>) {
         
     | 
| 
      
 19 
     | 
    
         
            +
              const cells = listData.cells;
         
     | 
| 
      
 20 
     | 
    
         
            +
              const utilCells = listData.list?.utilCells;
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
              return (
         
     | 
| 
      
 23 
     | 
    
         
            +
                <div className="datagrid">
         
     | 
| 
      
 24 
     | 
    
         
            +
                  {!data || data.length === 0 ? (
         
     | 
| 
      
 25 
     | 
    
         
            +
                    <EmptyList />
         
     | 
| 
      
 26 
     | 
    
         
            +
                  ) : (
         
     | 
| 
      
 27 
     | 
    
         
            +
                    <table className="datagrid-table">
         
     | 
| 
      
 28 
     | 
    
         
            +
                      <thead>
         
     | 
| 
      
 29 
     | 
    
         
            +
                        <tr>
         
     | 
| 
      
 30 
     | 
    
         
            +
                          {cells.map(cellOptions => (
         
     | 
| 
      
 31 
     | 
    
         
            +
                            <th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
         
     | 
| 
      
 32 
     | 
    
         
            +
                          ))}
         
     | 
| 
      
 33 
     | 
    
         
            +
                          {utilCells?.details && <th>Details</th>}
         
     | 
| 
      
 34 
     | 
    
         
            +
                          {utilCells?.edit && <th>Edit</th>}
         
     | 
| 
      
 35 
     | 
    
         
            +
                          {utilCells?.delete && <th>Delete</th>}
         
     | 
| 
      
 36 
     | 
    
         
            +
                        </tr>
         
     | 
| 
      
 37 
     | 
    
         
            +
                      </thead>
         
     | 
| 
      
 38 
     | 
    
         
            +
                      <tbody>
         
     | 
| 
      
 39 
     | 
    
         
            +
                        {data.map((item, index) => (
         
     | 
| 
      
 40 
     | 
    
         
            +
                          <tr key={index}>
         
     | 
| 
      
 41 
     | 
    
         
            +
                            {cells.map(cellOptions => {
         
     | 
| 
      
 42 
     | 
    
         
            +
                              // @ts-ignore
         
     | 
| 
      
 43 
     | 
    
         
            +
                              const value = item[cellOptions.name];
         
     | 
| 
      
 44 
     | 
    
         
            +
                              let render = value ?? '-'; // Default value if the field is undefined or null
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                              switch (cellOptions.type) {
         
     | 
| 
      
 47 
     | 
    
         
            +
                                case 'date':
         
     | 
| 
      
 48 
     | 
    
         
            +
                                  if (value) {
         
     | 
| 
      
 49 
     | 
    
         
            +
                                    const date = new Date(value);
         
     | 
| 
      
 50 
     | 
    
         
            +
                                    render = `${date.getDate().toString().padStart(2, '0')}/${(
         
     | 
| 
      
 51 
     | 
    
         
            +
                                      date.getMonth() + 1
         
     | 
| 
      
 52 
     | 
    
         
            +
                                    )
         
     | 
| 
      
 53 
     | 
    
         
            +
                                      .toString()
         
     | 
| 
      
 54 
     | 
    
         
            +
                                      .padStart(
         
     | 
| 
      
 55 
     | 
    
         
            +
                                        2,
         
     | 
| 
      
 56 
     | 
    
         
            +
                                        '0'
         
     | 
| 
      
 57 
     | 
    
         
            +
                                      )}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date
         
     | 
| 
      
 58 
     | 
    
         
            +
                                      .getMinutes()
         
     | 
| 
      
 59 
     | 
    
         
            +
                                      .toString()
         
     | 
| 
      
 60 
     | 
    
         
            +
                                      .padStart(2, '0')}`;
         
     | 
| 
      
 61 
     | 
    
         
            +
                                  }
         
     | 
| 
      
 62 
     | 
    
         
            +
                                  break;
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                                case 'image': {
         
     | 
| 
      
 65 
     | 
    
         
            +
                                  const imageCellOptions = cellOptions as ImageCellOptions;
         
     | 
| 
      
 66 
     | 
    
         
            +
                                  render = (
         
     | 
| 
      
 67 
     | 
    
         
            +
                                    <img
         
     | 
| 
      
 68 
     | 
    
         
            +
                                      width={100}
         
     | 
| 
      
 69 
     | 
    
         
            +
                                      height={100}
         
     | 
| 
      
 70 
     | 
    
         
            +
                                      src={imageCellOptions.baseUrl + value}
         
     | 
| 
      
 71 
     | 
    
         
            +
                                      style={{ objectFit: 'contain' }}
         
     | 
| 
      
 72 
     | 
    
         
            +
                                    />
         
     | 
| 
      
 73 
     | 
    
         
            +
                                  );
         
     | 
| 
      
 74 
     | 
    
         
            +
                                  break;
         
     | 
| 
      
 75 
     | 
    
         
            +
                                }
         
     | 
| 
      
 76 
     | 
    
         
            +
                                case 'string':
         
     | 
| 
      
 77 
     | 
    
         
            +
                                default:
         
     | 
| 
      
 78 
     | 
    
         
            +
                                  render = value ? value.toString() : (cellOptions?.placeHolder ?? '-'); // Handles string type or default fallback
         
     | 
| 
      
 79 
     | 
    
         
            +
                                  break;
         
     | 
| 
      
 80 
     | 
    
         
            +
                              }
         
     | 
| 
      
 81 
     | 
    
         
            +
                              /*
         
     | 
| 
      
 82 
     | 
    
         
            +
            								if (cellOptions.linkTo) {
         
     | 
| 
      
 83 
     | 
    
         
            +
            									render = <Link to={cellOptions.linkTo(item)}>{formattedValue}</Link>;
         
     | 
| 
      
 84 
     | 
    
         
            +
            								}
         
     | 
| 
      
 85 
     | 
    
         
            +
            */
         
     | 
| 
      
 86 
     | 
    
         
            +
                              return <td key={cellOptions.name}>{render}</td>;
         
     | 
| 
      
 87 
     | 
    
         
            +
                            })}
         
     | 
| 
      
 88 
     | 
    
         
            +
                            {utilCells?.details && (
         
     | 
| 
      
 89 
     | 
    
         
            +
                              <td>
         
     | 
| 
      
 90 
     | 
    
         
            +
                                <Link to={`${utilCells.details.path}/${item.id}`} className="util-cell-link">
         
     | 
| 
      
 91 
     | 
    
         
            +
                                  <SearchIcon className="icon icon-search" />
         
     | 
| 
      
 92 
     | 
    
         
            +
                                  <span className="util-cell-label">{utilCells.details.label}</span>
         
     | 
| 
      
 93 
     | 
    
         
            +
                                </Link>
         
     | 
| 
      
 94 
     | 
    
         
            +
                              </td>
         
     | 
| 
      
 95 
     | 
    
         
            +
                            )}
         
     | 
| 
      
 96 
     | 
    
         
            +
                            {utilCells?.edit && (
         
     | 
| 
      
 97 
     | 
    
         
            +
                              <td>
         
     | 
| 
      
 98 
     | 
    
         
            +
                                <Link to={`${utilCells.edit.path}/${item.id}`} className="util-cell-link">
         
     | 
| 
      
 99 
     | 
    
         
            +
                                  <PencilIcon className="icon icon-pencil" />
         
     | 
| 
      
 100 
     | 
    
         
            +
                                  <span className="util-cell-label">{utilCells.edit.label}</span>
         
     | 
| 
      
 101 
     | 
    
         
            +
                                </Link>
         
     | 
| 
      
 102 
     | 
    
         
            +
                              </td>
         
     | 
| 
      
 103 
     | 
    
         
            +
                            )}
         
     | 
| 
      
 104 
     | 
    
         
            +
                            {utilCells?.delete && (
         
     | 
| 
      
 105 
     | 
    
         
            +
                              <td>
         
     | 
| 
      
 106 
     | 
    
         
            +
                                <a onClick={() => {
         
     | 
| 
      
 107 
     | 
    
         
            +
                                  onRemoveItem?.(item)
         
     | 
| 
      
 108 
     | 
    
         
            +
                                }} className="util-cell-link">
         
     | 
| 
      
 109 
     | 
    
         
            +
                                  <TrashIcon className="icon icon-trash" />
         
     | 
| 
      
 110 
     | 
    
         
            +
                                  <span className="util-cell-label">{utilCells.delete.label}</span>
         
     | 
| 
      
 111 
     | 
    
         
            +
                                </a>
         
     | 
| 
      
 112 
     | 
    
         
            +
                              </td>
         
     | 
| 
      
 113 
     | 
    
         
            +
                            )}
         
     | 
| 
      
 114 
     | 
    
         
            +
                          </tr>
         
     | 
| 
      
 115 
     | 
    
         
            +
                        ))}
         
     | 
| 
      
 116 
     | 
    
         
            +
                      </tbody>
         
     | 
| 
      
 117 
     | 
    
         
            +
                    </table>
         
     | 
| 
      
 118 
     | 
    
         
            +
                  )}
         
     | 
| 
      
 119 
     | 
    
         
            +
                </div>
         
     | 
| 
      
 120 
     | 
    
         
            +
              );
         
     | 
| 
      
 121 
     | 
    
         
            +
            }
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import React from 'react';
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            export const EmptyList: React.FC = () => {
         
     | 
| 
      
 4 
     | 
    
         
            +
              return (
         
     | 
| 
      
 5 
     | 
    
         
            +
                <div className="empty-list">
         
     | 
| 
      
 6 
     | 
    
         
            +
                  <div className="empty-list-content">
         
     | 
| 
      
 7 
     | 
    
         
            +
                    <svg
         
     | 
| 
      
 8 
     | 
    
         
            +
                      xmlns="http://www.w3.org/2000/svg"
         
     | 
| 
      
 9 
     | 
    
         
            +
                      width="64"
         
     | 
| 
      
 10 
     | 
    
         
            +
                      height="64"
         
     | 
| 
      
 11 
     | 
    
         
            +
                      viewBox="0 0 24 24"
         
     | 
| 
      
 12 
     | 
    
         
            +
                      fill="none"
         
     | 
| 
      
 13 
     | 
    
         
            +
                      stroke="currentColor"
         
     | 
| 
      
 14 
     | 
    
         
            +
                      strokeWidth="2"
         
     | 
| 
      
 15 
     | 
    
         
            +
                      strokeLinecap="round"
         
     | 
| 
      
 16 
     | 
    
         
            +
                      strokeLinejoin="round"
         
     | 
| 
      
 17 
     | 
    
         
            +
                    >
         
     | 
| 
      
 18 
     | 
    
         
            +
                      <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
         
     | 
| 
      
 19 
     | 
    
         
            +
                      <polyline points="13 2 13 9 20 9" />
         
     | 
| 
      
 20 
     | 
    
         
            +
                    </svg>
         
     | 
| 
      
 21 
     | 
    
         
            +
                    <h3>No Data Found</h3>
         
     | 
| 
      
 22 
     | 
    
         
            +
                    <p>There are no items to display at the moment.</p>
         
     | 
| 
      
 23 
     | 
    
         
            +
                  </div>
         
     | 
| 
      
 24 
     | 
    
         
            +
                </div>
         
     | 
| 
      
 25 
     | 
    
         
            +
              );
         
     | 
| 
      
 26 
     | 
    
         
            +
            };
         
     | 
| 
         @@ -0,0 +1,202 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import React, { useEffect, useMemo, useRef } from 'react';
         
     | 
| 
      
 2 
     | 
    
         
            +
            import { ListData } from '../../../decorators/list/ListData';
         
     | 
| 
      
 3 
     | 
    
         
            +
            import { CellOptions, StaticSelectFilter } from '../../../decorators/list/Cell';
         
     | 
| 
      
 4 
     | 
    
         
            +
            import Select from 'react-select';
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            interface FilterPopupProps {
         
     | 
| 
      
 7 
     | 
    
         
            +
              isOpen: boolean;
         
     | 
| 
      
 8 
     | 
    
         
            +
              onClose: () => void;
         
     | 
| 
      
 9 
     | 
    
         
            +
              onApplyFilters: (filters: Record<string, string>) => void;
         
     | 
| 
      
 10 
     | 
    
         
            +
              listData: ListData;
         
     | 
| 
      
 11 
     | 
    
         
            +
              activeFilters?: Record<string, string>;
         
     | 
| 
      
 12 
     | 
    
         
            +
            }
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            interface FilterFieldProps {
         
     | 
| 
      
 15 
     | 
    
         
            +
              field: CellOptions;
         
     | 
| 
      
 16 
     | 
    
         
            +
              value: string;
         
     | 
| 
      
 17 
     | 
    
         
            +
              onChange: (value: string) => void;
         
     | 
| 
      
 18 
     | 
    
         
            +
            }
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            function FilterField({ field, value, onChange }: FilterFieldProps): React.ReactElement {
         
     | 
| 
      
 21 
     | 
    
         
            +
              switch (field.filter?.type) {
         
     | 
| 
      
 22 
     | 
    
         
            +
                case 'static-select': {
         
     | 
| 
      
 23 
     | 
    
         
            +
                  const filter = field.filter as StaticSelectFilter;
         
     | 
| 
      
 24 
     | 
    
         
            +
                  return (
         
     | 
| 
      
 25 
     | 
    
         
            +
                    <Select
         
     | 
| 
      
 26 
     | 
    
         
            +
                      id={field.name}
         
     | 
| 
      
 27 
     | 
    
         
            +
                      menuPortalTarget={document.body}
         
     | 
| 
      
 28 
     | 
    
         
            +
                      styles={{
         
     | 
| 
      
 29 
     | 
    
         
            +
                        control: (baseStyles, state) => ({
         
     | 
| 
      
 30 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 31 
     | 
    
         
            +
                          backgroundColor: '#1f2937',
         
     | 
| 
      
 32 
     | 
    
         
            +
                          borderColor: state.isFocused ? '#6366f1' : '#374151',
         
     | 
| 
      
 33 
     | 
    
         
            +
                          boxShadow: state.isFocused ? '0 0 0 1px #6366f1' : 'none',
         
     | 
| 
      
 34 
     | 
    
         
            +
                          '&:hover': {
         
     | 
| 
      
 35 
     | 
    
         
            +
                            borderColor: '#6366f1',
         
     | 
| 
      
 36 
     | 
    
         
            +
                          },
         
     | 
| 
      
 37 
     | 
    
         
            +
                          borderRadius: '6px',
         
     | 
| 
      
 38 
     | 
    
         
            +
                          padding: '2px',
         
     | 
| 
      
 39 
     | 
    
         
            +
                          color: 'white',
         
     | 
| 
      
 40 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 41 
     | 
    
         
            +
                        option: (baseStyles, state) => ({
         
     | 
| 
      
 42 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 43 
     | 
    
         
            +
                          backgroundColor: state.isSelected
         
     | 
| 
      
 44 
     | 
    
         
            +
                            ? '#6366f1'
         
     | 
| 
      
 45 
     | 
    
         
            +
                            : state.isFocused
         
     | 
| 
      
 46 
     | 
    
         
            +
                              ? '#374151'
         
     | 
| 
      
 47 
     | 
    
         
            +
                              : '#1f2937',
         
     | 
| 
      
 48 
     | 
    
         
            +
                          color: 'white',
         
     | 
| 
      
 49 
     | 
    
         
            +
                          '&:active': {
         
     | 
| 
      
 50 
     | 
    
         
            +
                            backgroundColor: '#6366f1',
         
     | 
| 
      
 51 
     | 
    
         
            +
                          },
         
     | 
| 
      
 52 
     | 
    
         
            +
                          '&:hover': {
         
     | 
| 
      
 53 
     | 
    
         
            +
                            backgroundColor: '#374151',
         
     | 
| 
      
 54 
     | 
    
         
            +
                          },
         
     | 
| 
      
 55 
     | 
    
         
            +
                          cursor: 'pointer',
         
     | 
| 
      
 56 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 57 
     | 
    
         
            +
                        input: baseStyles => ({
         
     | 
| 
      
 58 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 59 
     | 
    
         
            +
                          color: 'white',
         
     | 
| 
      
 60 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 61 
     | 
    
         
            +
                        placeholder: baseStyles => ({
         
     | 
| 
      
 62 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 63 
     | 
    
         
            +
                          color: '#9ca3af',
         
     | 
| 
      
 64 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 65 
     | 
    
         
            +
                        singleValue: baseStyles => ({
         
     | 
| 
      
 66 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 67 
     | 
    
         
            +
                          color: 'white',
         
     | 
| 
      
 68 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 69 
     | 
    
         
            +
                        menuPortal: baseStyles => ({
         
     | 
| 
      
 70 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 71 
     | 
    
         
            +
                          zIndex: 9999,
         
     | 
| 
      
 72 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 73 
     | 
    
         
            +
                        menu: baseStyles => ({
         
     | 
| 
      
 74 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 75 
     | 
    
         
            +
                          backgroundColor: '#1f2937',
         
     | 
| 
      
 76 
     | 
    
         
            +
                          border: '1px solid #374151',
         
     | 
| 
      
 77 
     | 
    
         
            +
                          boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
         
     | 
| 
      
 78 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 79 
     | 
    
         
            +
                        menuList: baseStyles => ({
         
     | 
| 
      
 80 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 81 
     | 
    
         
            +
                          padding: '4px',
         
     | 
| 
      
 82 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 83 
     | 
    
         
            +
                        dropdownIndicator: baseStyles => ({
         
     | 
| 
      
 84 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 85 
     | 
    
         
            +
                          color: '#9ca3af',
         
     | 
| 
      
 86 
     | 
    
         
            +
                          '&:hover': {
         
     | 
| 
      
 87 
     | 
    
         
            +
                            color: '#6366f1',
         
     | 
| 
      
 88 
     | 
    
         
            +
                          },
         
     | 
| 
      
 89 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 90 
     | 
    
         
            +
                        clearIndicator: baseStyles => ({
         
     | 
| 
      
 91 
     | 
    
         
            +
                          ...baseStyles,
         
     | 
| 
      
 92 
     | 
    
         
            +
                          color: '#9ca3af',
         
     | 
| 
      
 93 
     | 
    
         
            +
                          '&:hover': {
         
     | 
| 
      
 94 
     | 
    
         
            +
                            color: '#6366f1',
         
     | 
| 
      
 95 
     | 
    
         
            +
                          },
         
     | 
| 
      
 96 
     | 
    
         
            +
                        }),
         
     | 
| 
      
 97 
     | 
    
         
            +
                      }}
         
     | 
| 
      
 98 
     | 
    
         
            +
                      value={
         
     | 
| 
      
 99 
     | 
    
         
            +
                        value
         
     | 
| 
      
 100 
     | 
    
         
            +
                          ? {
         
     | 
| 
      
 101 
     | 
    
         
            +
                              value: value,
         
     | 
| 
      
 102 
     | 
    
         
            +
                              label: filter.options.find(opt => opt.value === value)?.label || value,
         
     | 
| 
      
 103 
     | 
    
         
            +
                            }
         
     | 
| 
      
 104 
     | 
    
         
            +
                          : null
         
     | 
| 
      
 105 
     | 
    
         
            +
                      }
         
     | 
| 
      
 106 
     | 
    
         
            +
                      onChange={option => onChange(option?.value || '')}
         
     | 
| 
      
 107 
     | 
    
         
            +
                      options={filter.options.map(opt => ({
         
     | 
| 
      
 108 
     | 
    
         
            +
                        value: opt.value,
         
     | 
| 
      
 109 
     | 
    
         
            +
                        label: opt.label,
         
     | 
| 
      
 110 
     | 
    
         
            +
                      }))}
         
     | 
| 
      
 111 
     | 
    
         
            +
                      placeholder={`Filter by ${field.title || field.name}`}
         
     | 
| 
      
 112 
     | 
    
         
            +
                      isClearable
         
     | 
| 
      
 113 
     | 
    
         
            +
                    />
         
     | 
| 
      
 114 
     | 
    
         
            +
                  );
         
     | 
| 
      
 115 
     | 
    
         
            +
                }
         
     | 
| 
      
 116 
     | 
    
         
            +
                default:
         
     | 
| 
      
 117 
     | 
    
         
            +
                  return (
         
     | 
| 
      
 118 
     | 
    
         
            +
                    <input
         
     | 
| 
      
 119 
     | 
    
         
            +
                      type={field.type === 'number' ? 'number' : 'text'}
         
     | 
| 
      
 120 
     | 
    
         
            +
                      id={field.name}
         
     | 
| 
      
 121 
     | 
    
         
            +
                      value={value || ''}
         
     | 
| 
      
 122 
     | 
    
         
            +
                      onChange={e => onChange(e.target.value)}
         
     | 
| 
      
 123 
     | 
    
         
            +
                      placeholder={`Filter by ${field.title || field.name}`}
         
     | 
| 
      
 124 
     | 
    
         
            +
                    />
         
     | 
| 
      
 125 
     | 
    
         
            +
                  );
         
     | 
| 
      
 126 
     | 
    
         
            +
              }
         
     | 
| 
      
 127 
     | 
    
         
            +
            }
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
            export function FilterPopup({
         
     | 
| 
      
 130 
     | 
    
         
            +
              isOpen,
         
     | 
| 
      
 131 
     | 
    
         
            +
              onClose,
         
     | 
| 
      
 132 
     | 
    
         
            +
              onApplyFilters,
         
     | 
| 
      
 133 
     | 
    
         
            +
              listData,
         
     | 
| 
      
 134 
     | 
    
         
            +
              activeFilters,
         
     | 
| 
      
 135 
     | 
    
         
            +
            }: FilterPopupProps): React.ReactElement | null {
         
     | 
| 
      
 136 
     | 
    
         
            +
              const [filters, setFilters] = React.useState<Record<string, any>>(activeFilters ?? {});
         
     | 
| 
      
 137 
     | 
    
         
            +
              const popupRef = useRef<HTMLDivElement>(null);
         
     | 
| 
      
 138 
     | 
    
         
            +
              const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
              useEffect(() => {
         
     | 
| 
      
 141 
     | 
    
         
            +
                const handleClickOutside = (event: MouseEvent) => {
         
     | 
| 
      
 142 
     | 
    
         
            +
                  if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
         
     | 
| 
      
 143 
     | 
    
         
            +
                    onClose();
         
     | 
| 
      
 144 
     | 
    
         
            +
                  }
         
     | 
| 
      
 145 
     | 
    
         
            +
                };
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                if (isOpen) {
         
     | 
| 
      
 148 
     | 
    
         
            +
                  document.addEventListener('mousedown', handleClickOutside);
         
     | 
| 
      
 149 
     | 
    
         
            +
                }
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
                return () => {
         
     | 
| 
      
 152 
     | 
    
         
            +
                  document.removeEventListener('mousedown', handleClickOutside);
         
     | 
| 
      
 153 
     | 
    
         
            +
                };
         
     | 
| 
      
 154 
     | 
    
         
            +
              }, [isOpen, onClose]);
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
              if (!isOpen) return null;
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
              const handleFilterChange = (fieldName: string, value: any) => {
         
     | 
| 
      
 159 
     | 
    
         
            +
                setFilters(prev => ({
         
     | 
| 
      
 160 
     | 
    
         
            +
                  ...prev,
         
     | 
| 
      
 161 
     | 
    
         
            +
                  [fieldName]: value,
         
     | 
| 
      
 162 
     | 
    
         
            +
                }));
         
     | 
| 
      
 163 
     | 
    
         
            +
              };
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
              const handleApply = () => {
         
     | 
| 
      
 166 
     | 
    
         
            +
                onApplyFilters(filters);
         
     | 
| 
      
 167 
     | 
    
         
            +
                onClose();
         
     | 
| 
      
 168 
     | 
    
         
            +
              };
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
              return (
         
     | 
| 
      
 171 
     | 
    
         
            +
                <div className="filter-popup-overlay">
         
     | 
| 
      
 172 
     | 
    
         
            +
                  <div ref={popupRef} className="filter-popup">
         
     | 
| 
      
 173 
     | 
    
         
            +
                    <div className="filter-popup-header">
         
     | 
| 
      
 174 
     | 
    
         
            +
                      <h3>Filter</h3>
         
     | 
| 
      
 175 
     | 
    
         
            +
                      <button onClick={onClose} className="close-button">
         
     | 
| 
      
 176 
     | 
    
         
            +
                        ×
         
     | 
| 
      
 177 
     | 
    
         
            +
                      </button>
         
     | 
| 
      
 178 
     | 
    
         
            +
                    </div>
         
     | 
| 
      
 179 
     | 
    
         
            +
                    <div className="filter-popup-content">
         
     | 
| 
      
 180 
     | 
    
         
            +
                      {fields.map((field: CellOptions) => (
         
     | 
| 
      
 181 
     | 
    
         
            +
                        <div key={field.name} className="filter-field">
         
     | 
| 
      
 182 
     | 
    
         
            +
                          <label htmlFor={field.name}>{field.title || field.name}</label>
         
     | 
| 
      
 183 
     | 
    
         
            +
                          <FilterField
         
     | 
| 
      
 184 
     | 
    
         
            +
                            field={field}
         
     | 
| 
      
 185 
     | 
    
         
            +
                            value={filters[field.name || '']}
         
     | 
| 
      
 186 
     | 
    
         
            +
                            onChange={value => handleFilterChange(field.name || '', value)}
         
     | 
| 
      
 187 
     | 
    
         
            +
                          />
         
     | 
| 
      
 188 
     | 
    
         
            +
                        </div>
         
     | 
| 
      
 189 
     | 
    
         
            +
                      ))}
         
     | 
| 
      
 190 
     | 
    
         
            +
                    </div>
         
     | 
| 
      
 191 
     | 
    
         
            +
                    <div className="filter-popup-footer">
         
     | 
| 
      
 192 
     | 
    
         
            +
                      <button onClick={onClose} className="cancel-button">
         
     | 
| 
      
 193 
     | 
    
         
            +
                        Cancel
         
     | 
| 
      
 194 
     | 
    
         
            +
                      </button>
         
     | 
| 
      
 195 
     | 
    
         
            +
                      <button onClick={handleApply} className="apply-button">
         
     | 
| 
      
 196 
     | 
    
         
            +
                        Apply Filters
         
     | 
| 
      
 197 
     | 
    
         
            +
                      </button>
         
     | 
| 
      
 198 
     | 
    
         
            +
                    </div>
         
     | 
| 
      
 199 
     | 
    
         
            +
                  </div>
         
     | 
| 
      
 200 
     | 
    
         
            +
                </div>
         
     | 
| 
      
 201 
     | 
    
         
            +
              );
         
     | 
| 
      
 202 
     | 
    
         
            +
            }
         
     | 
| 
         @@ -0,0 +1,178 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import React, { useMemo, useCallback, useEffect, useState } from 'react';
         
     | 
| 
      
 2 
     | 
    
         
            +
            import { Link, useParams, useNavigate } from 'react-router';
         
     | 
| 
      
 3 
     | 
    
         
            +
            import { Datagrid } from './Datagrid';
         
     | 
| 
      
 4 
     | 
    
         
            +
            import { ErrorComponent } from '../ErrorComponent';
         
     | 
| 
      
 5 
     | 
    
         
            +
            import { LoadingScreen } from '../LoadingScreen';
         
     | 
| 
      
 6 
     | 
    
         
            +
            import { AnyClass } from '../../../types/AnyClass';
         
     | 
| 
      
 7 
     | 
    
         
            +
            import { getListFields } from '../../../decorators/list/getListFields';
         
     | 
| 
      
 8 
     | 
    
         
            +
            import { Pagination } from './Pagination';
         
     | 
| 
      
 9 
     | 
    
         
            +
            import { ListData } from '../../../decorators/list/ListData';
         
     | 
| 
      
 10 
     | 
    
         
            +
            import CreateIcon from '../../../assets/icons/svg/create.svg';
         
     | 
| 
      
 11 
     | 
    
         
            +
            import FilterIcon from '../../../assets/icons/svg/filter.svg';
         
     | 
| 
      
 12 
     | 
    
         
            +
            import { FilterPopup } from './FilterPopup';
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            export interface GetDataParams {
         
     | 
| 
      
 15 
     | 
    
         
            +
              page?: number;
         
     | 
| 
      
 16 
     | 
    
         
            +
              limit?: number;
         
     | 
| 
      
 17 
     | 
    
         
            +
              filters?: Record<string, any>;
         
     | 
| 
      
 18 
     | 
    
         
            +
            }
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            export interface PaginatedResponse<T> {
         
     | 
| 
      
 21 
     | 
    
         
            +
              data: T[];
         
     | 
| 
      
 22 
     | 
    
         
            +
              total: number;
         
     | 
| 
      
 23 
     | 
    
         
            +
              page: number;
         
     | 
| 
      
 24 
     | 
    
         
            +
              limit: number;
         
     | 
| 
      
 25 
     | 
    
         
            +
            }
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
            export type GetDataForList<T> = (params: GetDataParams) => Promise<PaginatedResponse<T>>;
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
            const ListHeader = ({
         
     | 
| 
      
 30 
     | 
    
         
            +
              listData,
         
     | 
| 
      
 31 
     | 
    
         
            +
              filtered,
         
     | 
| 
      
 32 
     | 
    
         
            +
              onFilterClick,
         
     | 
| 
      
 33 
     | 
    
         
            +
              customHeader,
         
     | 
| 
      
 34 
     | 
    
         
            +
            }: {
         
     | 
| 
      
 35 
     | 
    
         
            +
              listData: ListData;
         
     | 
| 
      
 36 
     | 
    
         
            +
              filtered: boolean;
         
     | 
| 
      
 37 
     | 
    
         
            +
              onFilterClick: () => void;
         
     | 
| 
      
 38 
     | 
    
         
            +
              customHeader?: React.ReactNode;
         
     | 
| 
      
 39 
     | 
    
         
            +
            }) => {
         
     | 
| 
      
 40 
     | 
    
         
            +
              const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
              const header = listData.list?.headers;
         
     | 
| 
      
 43 
     | 
    
         
            +
              return (
         
     | 
| 
      
 44 
     | 
    
         
            +
                <div className="list-header">
         
     | 
| 
      
 45 
     | 
    
         
            +
                  <div className="header-title">{header?.title || 'List'}</div>
         
     | 
| 
      
 46 
     | 
    
         
            +
                  {customHeader && <div className="header-custom">{customHeader}</div>}
         
     | 
| 
      
 47 
     | 
    
         
            +
                  <div className="header-actions">
         
     | 
| 
      
 48 
     | 
    
         
            +
                    {!!fields.length && (
         
     | 
| 
      
 49 
     | 
    
         
            +
                      <button onClick={onFilterClick} className="filter-button">
         
     | 
| 
      
 50 
     | 
    
         
            +
                        <FilterIcon className={`icon icon-filter ${filtered ? 'active' : ''}`} />
         
     | 
| 
      
 51 
     | 
    
         
            +
                        Filter
         
     | 
| 
      
 52 
     | 
    
         
            +
                      </button>
         
     | 
| 
      
 53 
     | 
    
         
            +
                    )}
         
     | 
| 
      
 54 
     | 
    
         
            +
                    {header?.create && (
         
     | 
| 
      
 55 
     | 
    
         
            +
                      <Link to={header.create.path} className="create-button">
         
     | 
| 
      
 56 
     | 
    
         
            +
                        <CreateIcon className="icon icon-create" />
         
     | 
| 
      
 57 
     | 
    
         
            +
                        {header.create.label}
         
     | 
| 
      
 58 
     | 
    
         
            +
                      </Link>
         
     | 
| 
      
 59 
     | 
    
         
            +
                    )}
         
     | 
| 
      
 60 
     | 
    
         
            +
                  </div>
         
     | 
| 
      
 61 
     | 
    
         
            +
                </div>
         
     | 
| 
      
 62 
     | 
    
         
            +
              );
         
     | 
| 
      
 63 
     | 
    
         
            +
            };
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
            export function ListPage<T extends AnyClass & { id: string }>({
         
     | 
| 
      
 66 
     | 
    
         
            +
              model,
         
     | 
| 
      
 67 
     | 
    
         
            +
              getData,
         
     | 
| 
      
 68 
     | 
    
         
            +
              onRemoveItem,
         
     | 
| 
      
 69 
     | 
    
         
            +
              customHeader,
         
     | 
| 
      
 70 
     | 
    
         
            +
            }: {
         
     | 
| 
      
 71 
     | 
    
         
            +
              model: T;
         
     | 
| 
      
 72 
     | 
    
         
            +
              getData: GetDataForList<T>;
         
     | 
| 
      
 73 
     | 
    
         
            +
              customHeader?: React.ReactNode;
         
     | 
| 
      
 74 
     | 
    
         
            +
              onRemoveItem?: (item: T) => Promise<void>;
         
     | 
| 
      
 75 
     | 
    
         
            +
            }) {
         
     | 
| 
      
 76 
     | 
    
         
            +
              const [loading, setLoading] = useState(true);
         
     | 
| 
      
 77 
     | 
    
         
            +
              const [pagination, setPagination] = useState({ total: 0, page: 0, limit: 0 });
         
     | 
| 
      
 78 
     | 
    
         
            +
              const [data, setData] = useState<any>(null);
         
     | 
| 
      
 79 
     | 
    
         
            +
              const [error, setError] = useState<unknown>(null);
         
     | 
| 
      
 80 
     | 
    
         
            +
              const [isFilterOpen, setIsFilterOpen] = useState(false);
         
     | 
| 
      
 81 
     | 
    
         
            +
              const [activeFilters, setActiveFilters] = useState<Record<string, string>>();
         
     | 
| 
      
 82 
     | 
    
         
            +
              const listData = useMemo(() => getListFields(model), [model]);
         
     | 
| 
      
 83 
     | 
    
         
            +
              const params = useParams();
         
     | 
| 
      
 84 
     | 
    
         
            +
              const navigate = useNavigate();
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
              const fetchData = useCallback(
         
     | 
| 
      
 87 
     | 
    
         
            +
                async (page: number, filters?: Record<string, string>) => {
         
     | 
| 
      
 88 
     | 
    
         
            +
                  setLoading(true);
         
     | 
| 
      
 89 
     | 
    
         
            +
                  try {
         
     | 
| 
      
 90 
     | 
    
         
            +
                    const result = await getData({ page, filters: filters ?? activeFilters ?? {} });
         
     | 
| 
      
 91 
     | 
    
         
            +
                    setData(result.data);
         
     | 
| 
      
 92 
     | 
    
         
            +
                    setPagination({
         
     | 
| 
      
 93 
     | 
    
         
            +
                      total: result.total,
         
     | 
| 
      
 94 
     | 
    
         
            +
                      page: result.page,
         
     | 
| 
      
 95 
     | 
    
         
            +
                      limit: result.limit,
         
     | 
| 
      
 96 
     | 
    
         
            +
                    });
         
     | 
| 
      
 97 
     | 
    
         
            +
                  } catch (e) {
         
     | 
| 
      
 98 
     | 
    
         
            +
                    setError(e);
         
     | 
| 
      
 99 
     | 
    
         
            +
                    console.error(e);
         
     | 
| 
      
 100 
     | 
    
         
            +
                  } finally {
         
     | 
| 
      
 101 
     | 
    
         
            +
                    setLoading(false);
         
     | 
| 
      
 102 
     | 
    
         
            +
                  }
         
     | 
| 
      
 103 
     | 
    
         
            +
                },
         
     | 
| 
      
 104 
     | 
    
         
            +
                [getData, activeFilters]
         
     | 
| 
      
 105 
     | 
    
         
            +
              );
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
              useEffect(() => {
         
     | 
| 
      
 108 
     | 
    
         
            +
                const searchParams = new URLSearchParams(location.search);
         
     | 
| 
      
 109 
     | 
    
         
            +
                const filtersFromUrl: Record<string, string> = {};
         
     | 
| 
      
 110 
     | 
    
         
            +
                searchParams.forEach((value, key) => {
         
     | 
| 
      
 111 
     | 
    
         
            +
                  filtersFromUrl[key] = value;
         
     | 
| 
      
 112 
     | 
    
         
            +
                });
         
     | 
| 
      
 113 
     | 
    
         
            +
                setActiveFilters(filtersFromUrl);
         
     | 
| 
      
 114 
     | 
    
         
            +
              }, [location.search]);
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
              useEffect(() => {
         
     | 
| 
      
 117 
     | 
    
         
            +
                if (activeFilters) {
         
     | 
| 
      
 118 
     | 
    
         
            +
                  fetchData(parseInt(params.page as string) || 1, activeFilters);
         
     | 
| 
      
 119 
     | 
    
         
            +
                }
         
     | 
| 
      
 120 
     | 
    
         
            +
              }, [fetchData, params.page, activeFilters]);
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
              const handleFilterApply = (filters: Record<string, any>) => {
         
     | 
| 
      
 123 
     | 
    
         
            +
                setActiveFilters(filters);
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                // Convert filters to URLSearchParams
         
     | 
| 
      
 126 
     | 
    
         
            +
                const searchParams = new URLSearchParams();
         
     | 
| 
      
 127 
     | 
    
         
            +
                Object.entries(filters).forEach(([key, value]) => {
         
     | 
| 
      
 128 
     | 
    
         
            +
                  if (value !== undefined && value !== null && value !== '') {
         
     | 
| 
      
 129 
     | 
    
         
            +
                    searchParams.append(key, String(value));
         
     | 
| 
      
 130 
     | 
    
         
            +
                  }
         
     | 
| 
      
 131 
     | 
    
         
            +
                });
         
     | 
| 
      
 132 
     | 
    
         
            +
                const queryString = searchParams.toString();
         
     | 
| 
      
 133 
     | 
    
         
            +
                const newUrl = `${location.pathname}${queryString ? `?${queryString}` : ''}`;
         
     | 
| 
      
 134 
     | 
    
         
            +
                navigate(newUrl);
         
     | 
| 
      
 135 
     | 
    
         
            +
                fetchData(1, filters); // Reset to first page when filters change
         
     | 
| 
      
 136 
     | 
    
         
            +
              };
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
              if (loading) return <LoadingScreen />;
         
     | 
| 
      
 139 
     | 
    
         
            +
              if (error) return <ErrorComponent error={error} />;
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
              return (
         
     | 
| 
      
 142 
     | 
    
         
            +
                <div className="list">
         
     | 
| 
      
 143 
     | 
    
         
            +
                  <ListHeader
         
     | 
| 
      
 144 
     | 
    
         
            +
                    listData={listData}
         
     | 
| 
      
 145 
     | 
    
         
            +
                    filtered={!!(activeFilters && !!Object.keys(activeFilters).length)}
         
     | 
| 
      
 146 
     | 
    
         
            +
                    onFilterClick={() => setIsFilterOpen(true)}
         
     | 
| 
      
 147 
     | 
    
         
            +
                    customHeader={customHeader}
         
     | 
| 
      
 148 
     | 
    
         
            +
                  />
         
     | 
| 
      
 149 
     | 
    
         
            +
                  <Datagrid
         
     | 
| 
      
 150 
     | 
    
         
            +
                    listData={listData}
         
     | 
| 
      
 151 
     | 
    
         
            +
                    data={data}
         
     | 
| 
      
 152 
     | 
    
         
            +
                    onRemoveItem={async (item: T) => {
         
     | 
| 
      
 153 
     | 
    
         
            +
                      if (onRemoveItem) {
         
     | 
| 
      
 154 
     | 
    
         
            +
                        alert({
         
     | 
| 
      
 155 
     | 
    
         
            +
                          title: 'Are you sure you want to delete this item?',
         
     | 
| 
      
 156 
     | 
    
         
            +
                          message: 'This action cannot be undone.',
         
     | 
| 
      
 157 
     | 
    
         
            +
                          onConfirm: async () => {
         
     | 
| 
      
 158 
     | 
    
         
            +
                            await onRemoveItem(item);
         
     | 
| 
      
 159 
     | 
    
         
            +
                            setData(data.filter((d: T) => d.id !== item.id));
         
     | 
| 
      
 160 
     | 
    
         
            +
                            await fetchData(pagination.page);
         
     | 
| 
      
 161 
     | 
    
         
            +
                          },
         
     | 
| 
      
 162 
     | 
    
         
            +
                        });
         
     | 
| 
      
 163 
     | 
    
         
            +
                      }
         
     | 
| 
      
 164 
     | 
    
         
            +
                    }}
         
     | 
| 
      
 165 
     | 
    
         
            +
                  />
         
     | 
| 
      
 166 
     | 
    
         
            +
                  <div className="list-footer">
         
     | 
| 
      
 167 
     | 
    
         
            +
                    <Pagination pagination={pagination} onPageChange={fetchData} />
         
     | 
| 
      
 168 
     | 
    
         
            +
                  </div>
         
     | 
| 
      
 169 
     | 
    
         
            +
                  <FilterPopup
         
     | 
| 
      
 170 
     | 
    
         
            +
                    isOpen={isFilterOpen}
         
     | 
| 
      
 171 
     | 
    
         
            +
                    activeFilters={activeFilters}
         
     | 
| 
      
 172 
     | 
    
         
            +
                    onClose={() => setIsFilterOpen(false)}
         
     | 
| 
      
 173 
     | 
    
         
            +
                    onApplyFilters={handleFilterApply}
         
     | 
| 
      
 174 
     | 
    
         
            +
                    listData={listData}
         
     | 
| 
      
 175 
     | 
    
         
            +
                  />
         
     | 
| 
      
 176 
     | 
    
         
            +
                </div>
         
     | 
| 
      
 177 
     | 
    
         
            +
              );
         
     | 
| 
      
 178 
     | 
    
         
            +
            }
         
     | 
| 
         @@ -3,7 +3,7 @@ import { InnerForm } from '../components'; 
     | 
|
| 
       3 
3 
     | 
    
         
             
            import { AnyClass } from '../../types/AnyClass';
         
     | 
| 
       4 
4 
     | 
    
         
             
            import { getFormFields } from '../../decorators/form/getFormFields';
         
     | 
| 
       5 
5 
     | 
    
         | 
| 
       6 
     | 
    
         
            -
            export type GetDetailsDataFN<T> = (param: string) => Promise<T>;
         
     | 
| 
      
 6 
     | 
    
         
            +
            export type GetDetailsDataFN<T> = (param: Record<string, string>) => Promise<T>;
         
     | 
| 
       7 
7 
     | 
    
         
             
            export type OnSubmitFN<T> = (data: T) => Promise<T>;
         
     | 
| 
       8 
8 
     | 
    
         | 
| 
       9 
9 
     | 
    
         
             
            export interface FormPageProps<T extends AnyClass> {
         
     | 
| 
         @@ -11,6 +11,7 @@ export interface FormPageProps<T extends AnyClass> { 
     | 
|
| 
       11 
11 
     | 
    
         
             
              getDetailsData?: GetDetailsDataFN<T>;
         
     | 
| 
       12 
12 
     | 
    
         
             
              redirect?: string;
         
     | 
| 
       13 
13 
     | 
    
         
             
              onSubmit: OnSubmitFN<T>;
         
     | 
| 
      
 14 
     | 
    
         
            +
              redirectBackOnSuccess?: boolean;
         
     | 
| 
       14 
15 
     | 
    
         
             
            }
         
     | 
| 
       15 
16 
     | 
    
         | 
| 
       16 
17 
     | 
    
         
             
            export function FormPage<T extends AnyClass>({
         
     | 
| 
         @@ -18,15 +19,16 @@ export function FormPage<T extends AnyClass>({ 
     | 
|
| 
       18 
19 
     | 
    
         
             
              getDetailsData,
         
     | 
| 
       19 
20 
     | 
    
         
             
              onSubmit,
         
     | 
| 
       20 
21 
     | 
    
         
             
              redirect,
         
     | 
| 
      
 22 
     | 
    
         
            +
              redirectBackOnSuccess = true,
         
     | 
| 
       21 
23 
     | 
    
         
             
              ...rest
         
     | 
| 
       22 
24 
     | 
    
         
             
            }: FormPageProps<T>) {
         
     | 
| 
       23 
25 
     | 
    
         
             
              const formOptions = useMemo(() => getFormFields(model), [model]);
         
     | 
| 
       24 
26 
     | 
    
         
             
              return (
         
     | 
| 
       25 
27 
     | 
    
         
             
                <InnerForm
         
     | 
| 
       26 
28 
     | 
    
         
             
                  getDetailsData={getDetailsData}
         
     | 
| 
       27 
     | 
    
         
            -
                  redirect={redirect}
         
     | 
| 
       28 
29 
     | 
    
         
             
                  onSubmit={onSubmit}
         
     | 
| 
       29 
30 
     | 
    
         
             
                  formOptions={formOptions}
         
     | 
| 
      
 31 
     | 
    
         
            +
                  redirectBackOnSuccess={redirectBackOnSuccess}
         
     | 
| 
       30 
32 
     | 
    
         
             
                />
         
     | 
| 
       31 
33 
     | 
    
         
             
              );
         
     | 
| 
       32 
34 
     | 
    
         
             
            }
         
     | 
| 
         @@ -8,13 +8,14 @@ const isFieldSensitive = (fieldName: string): boolean => { 
     | 
|
| 
       8 
8 
     | 
    
         
             
            };
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
       10 
10 
     | 
    
         
             
            export interface InputOptions {
         
     | 
| 
      
 11 
     | 
    
         
            +
              type?: 'input' | 'select' | 'textarea' | 'file-upload' | 'checkbox' | 'hidden' | 'nested';
         
     | 
| 
      
 12 
     | 
    
         
            +
              inputType?: 'text' | 'email' | 'tel' | 'password' | 'number' | 'date';
         
     | 
| 
       11 
13 
     | 
    
         
             
              name?: string;
         
     | 
| 
       12 
14 
     | 
    
         
             
              label?: string;
         
     | 
| 
       13 
15 
     | 
    
         
             
              placeholder?: string;
         
     | 
| 
       14 
     | 
    
         
            -
              inputType?: 'text' | 'email' | 'tel' | 'password' | 'number' | 'date';
         
     | 
| 
       15 
     | 
    
         
            -
              type?: 'input' | 'select' | 'textarea' | 'file-upload' | 'checkbox' | 'hidden';
         
     | 
| 
       16 
     | 
    
         
            -
              selectOptions?: string[]; //TODO: label/value
         
     | 
| 
       17 
16 
     | 
    
         
             
              cancelPasswordValidationOnEdit?: boolean;
         
     | 
| 
      
 17 
     | 
    
         
            +
              options?: { value: string; label: string }[];
         
     | 
| 
      
 18 
     | 
    
         
            +
              nestedFields?: InputOptions[];
         
     | 
| 
       18 
19 
     | 
    
         
             
            }
         
     | 
| 
       19 
20 
     | 
    
         | 
| 
       20 
21 
     | 
    
         
             
            export function Input(options?: InputOptions): PropertyDecorator {
         
     | 
| 
         @@ -1,22 +1,32 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            import  
     | 
| 
      
 1 
     | 
    
         
            +
            import 'reflect-metadata';
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            export const CELL_KEY = Symbol( 
     | 
| 
      
 3 
     | 
    
         
            +
            export const CELL_KEY = Symbol('cell');
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            interface Filter {
         
     | 
| 
      
 6 
     | 
    
         
            +
              type: 'string' | 'number' | 'date' | 'static-select';
         
     | 
| 
      
 7 
     | 
    
         
            +
            }
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
            export interface StaticSelectFilter extends Filter {
         
     | 
| 
      
 10 
     | 
    
         
            +
              type: 'static-select';
         
     | 
| 
      
 11 
     | 
    
         
            +
              options: { value: string; label: string }[];
         
     | 
| 
      
 12 
     | 
    
         
            +
            }
         
     | 
| 
       4 
13 
     | 
    
         | 
| 
       5 
14 
     | 
    
         
             
            export interface CellOptions {
         
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
      
 15 
     | 
    
         
            +
              name?: string;
         
     | 
| 
      
 16 
     | 
    
         
            +
              title?: string;
         
     | 
| 
      
 17 
     | 
    
         
            +
              type?: 'string' | 'date' | 'image' | 'number';
         
     | 
| 
      
 18 
     | 
    
         
            +
              placeHolder?: string;
         
     | 
| 
      
 19 
     | 
    
         
            +
              filter?: Filter | StaticSelectFilter;
         
     | 
| 
       10 
20 
     | 
    
         
             
            }
         
     | 
| 
       11 
21 
     | 
    
         | 
| 
       12 
22 
     | 
    
         
             
            export function Cell(options?: CellOptions): PropertyDecorator {
         
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
      
 23 
     | 
    
         
            +
              return (target, propertyKey) => {
         
     | 
| 
      
 24 
     | 
    
         
            +
                const existingCells: string[] = Reflect.getMetadata(CELL_KEY, target) || [];
         
     | 
| 
      
 25 
     | 
    
         
            +
                Reflect.defineMetadata(CELL_KEY, [...existingCells, propertyKey.toString()], target);
         
     | 
| 
       16 
26 
     | 
    
         | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
      
 27 
     | 
    
         
            +
                if (options) {
         
     | 
| 
      
 28 
     | 
    
         
            +
                  const keyString = `${CELL_KEY.toString()}:${propertyKey.toString()}:options`;
         
     | 
| 
      
 29 
     | 
    
         
            +
                  Reflect.defineMetadata(keyString, options, target);
         
     | 
| 
      
 30 
     | 
    
         
            +
                }
         
     | 
| 
      
 31 
     | 
    
         
            +
              };
         
     | 
| 
       22 
32 
     | 
    
         
             
            }
         
     | 
| 
         @@ -1,17 +1,31 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            import  
     | 
| 
      
 1 
     | 
    
         
            +
            import 'reflect-metadata';
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            const LIST_KEY =  
     | 
| 
      
 3 
     | 
    
         
            +
            const LIST_KEY = 'List';
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
       5 
     | 
    
         
            -
            export interface  
     | 
| 
      
 5 
     | 
    
         
            +
            export interface ListHeaderOptions {
         
     | 
| 
      
 6 
     | 
    
         
            +
              title?: string;
         
     | 
| 
      
 7 
     | 
    
         
            +
              create?: { path: string; label: string };
         
     | 
| 
      
 8 
     | 
    
         
            +
            }
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            export interface ListUtilCellOptions {
         
     | 
| 
      
 11 
     | 
    
         
            +
              details?: { path: string; label: string };
         
     | 
| 
      
 12 
     | 
    
         
            +
              edit?: { path: string; label: string };
         
     | 
| 
      
 13 
     | 
    
         
            +
              delete?: { path: string; label: string };
         
     | 
| 
      
 14 
     | 
    
         
            +
            }
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            export interface ListOptions {
         
     | 
| 
      
 17 
     | 
    
         
            +
              headers?: ListHeaderOptions;
         
     | 
| 
      
 18 
     | 
    
         
            +
              utilCells?: ListUtilCellOptions;
         
     | 
| 
      
 19 
     | 
    
         
            +
            }
         
     | 
| 
       6 
20 
     | 
    
         | 
| 
       7 
21 
     | 
    
         
             
            export function List(options?: ListOptions): ClassDecorator {
         
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
      
 22 
     | 
    
         
            +
              return (target: Function) => {
         
     | 
| 
      
 23 
     | 
    
         
            +
                if (options) {
         
     | 
| 
      
 24 
     | 
    
         
            +
                  Reflect.defineMetadata(LIST_KEY, options, target);
         
     | 
| 
      
 25 
     | 
    
         
            +
                }
         
     | 
| 
      
 26 
     | 
    
         
            +
              };
         
     | 
| 
       13 
27 
     | 
    
         
             
            }
         
     | 
| 
       14 
28 
     | 
    
         | 
| 
       15 
29 
     | 
    
         
             
            export function getClassListData(entityClass: any): ListOptions | undefined {
         
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
      
 30 
     | 
    
         
            +
              return Reflect.getMetadata(LIST_KEY, entityClass);
         
     | 
| 
       17 
31 
     | 
    
         
             
            }
         
     |