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.
Files changed (46) hide show
  1. package/.vscode/launch.json +10 -0
  2. package/dist/components/components/FormField.d.ts +2 -1
  3. package/dist/components/components/InnerForm.d.ts +2 -2
  4. package/dist/components/components/list/Datagrid.d.ts +13 -0
  5. package/dist/components/components/list/EmptyList.d.ts +2 -0
  6. package/dist/components/components/list/FilterPopup.d.ts +11 -0
  7. package/dist/components/components/list/ListPage.d.ts +22 -0
  8. package/dist/components/components/list/Pagination.d.ts +11 -0
  9. package/dist/components/components/list/index.d.ts +0 -0
  10. package/dist/components/list/Datagrid.d.ts +8 -4
  11. package/dist/components/list/EmptyList.d.ts +2 -0
  12. package/dist/components/list/FilterPopup.d.ts +10 -0
  13. package/dist/components/pages/FormPage.d.ts +3 -2
  14. package/dist/components/pages/ListPage.d.ts +2 -1
  15. package/dist/decorators/form/Input.d.ts +7 -3
  16. package/dist/decorators/list/Cell.d.ts +14 -2
  17. package/dist/decorators/list/List.d.ts +24 -1
  18. package/dist/index.cjs.js +1 -1
  19. package/dist/index.d.ts +4 -3
  20. package/dist/index.esm.js +1 -1
  21. package/package.json +9 -3
  22. package/src/assets/icons/svg/create.svg +9 -0
  23. package/src/assets/icons/svg/filter.svg +3 -0
  24. package/src/assets/icons/svg/pencil.svg +8 -0
  25. package/src/assets/icons/svg/search.svg +8 -0
  26. package/src/assets/icons/svg/trash.svg +8 -0
  27. package/src/components/components/FormField.tsx +41 -7
  28. package/src/components/components/InnerForm.tsx +8 -9
  29. package/src/components/components/list/Datagrid.tsx +121 -0
  30. package/src/components/components/list/EmptyList.tsx +26 -0
  31. package/src/components/components/list/FilterPopup.tsx +202 -0
  32. package/src/components/components/list/ListPage.tsx +178 -0
  33. package/src/components/pages/FormPage.tsx +4 -2
  34. package/src/decorators/form/Input.ts +4 -3
  35. package/src/decorators/list/Cell.ts +24 -14
  36. package/src/decorators/list/List.ts +23 -9
  37. package/src/index.ts +8 -3
  38. package/src/styles/filter-popup.scss +134 -0
  39. package/src/styles/index.scss +18 -22
  40. package/src/styles/list.scss +149 -8
  41. package/src/types/svg.d.ts +5 -0
  42. package/src/components/list/Datagrid.tsx +0 -101
  43. package/src/components/pages/ListPage.tsx +0 -85
  44. /package/src/components/{list → components/list}/Pagination.tsx +0 -0
  45. /package/src/components/{list → components/list}/index.ts +0 -0
  46. /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 "reflect-metadata";
1
+ import 'reflect-metadata';
2
2
 
3
- export const CELL_KEY = Symbol("cell");
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
- name?: string;
7
- title?: string;
8
- type?: "string" | "date" | "image";
9
- placeHolder?: string;
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
- return (target, propertyKey) => {
14
- const existingCells: string[] = Reflect.getMetadata(CELL_KEY, target) || [];
15
- Reflect.defineMetadata(CELL_KEY, [...existingCells, propertyKey.toString()], target);
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
- if (options) {
18
- const keyString = `${CELL_KEY.toString()}:${propertyKey.toString()}:options`;
19
- Reflect.defineMetadata(keyString, options, target);
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 "reflect-metadata";
1
+ import 'reflect-metadata';
2
2
 
3
- const LIST_KEY = "List";
3
+ const LIST_KEY = 'List';
4
4
 
5
- export interface ListOptions {}
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
- return (target: Function) => {
9
- if (options) {
10
- Reflect.defineMetadata(LIST_KEY, options, target);
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
- return Reflect.getMetadata(LIST_KEY, entityClass);
30
+ return Reflect.getMetadata(LIST_KEY, entityClass);
17
31
  }