Skip to content

Commit c57d770

Browse files
committed
Add Crud examples on Storybook and minor fix on RLCrud, RLDialog and RLCrudFilters
1 parent dcb0a85 commit c57d770

14 files changed

Lines changed: 594 additions & 8 deletions

File tree

src/components/RLCrud/RLCrud.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@ export const RLCrud = forwardRef<RLCrudRef, RLCrudProps>(
6464
: rowsPerPageOptions[0]
6565
)
6666
const [totalRows, setTotalRows] = useState(0)
67-
const [filtersApplied, setFiltersApplied] = useState<Record<string, unknown>>({})
67+
const [filtersApplied, setFiltersApplied] = useState<Record<string, unknown>>(() =>
68+
filtersConfig.reduce(
69+
(acc, filter) => ({
70+
...acc,
71+
[filter.value]: filter.default_value
72+
}),
73+
{}
74+
)
75+
)
6876
const [showDialog, setShowDialog] = useState(false)
6977
const [dialog, setDialog] = useState<string | null>(null)
7078
const [dialogProps, setDialogProps] = useState<Record<string, unknown>>({})
@@ -169,7 +177,7 @@ export const RLCrud = forwardRef<RLCrudRef, RLCrudProps>(
169177

170178
const onFiltersApplied = useCallback((appliedFilters: Record<string, unknown>) => {
171179
setFiltersApplied(appliedFilters)
172-
setCurrentPage(1)
180+
setCurrentPage(0)
173181
}, [])
174182

175183
const onConfirm = useCallback(async () => {
@@ -207,7 +215,7 @@ export const RLCrud = forwardRef<RLCrudRef, RLCrudProps>(
207215
setFiltersApplied({ [primary_key]: newId as RLCrudInputValueType })
208216
filtersRef.current?.setFilterModel({ [primary_key]: newId as RLCrudInputValueType })
209217
filtersRef.current?.setOpen(true)
210-
setCurrentPage(1)
218+
setCurrentPage(0)
211219
await Promise.resolve() // Allow state to update
212220
skipWatchersRef.current = false
213221
}
@@ -318,8 +326,8 @@ export const RLCrud = forwardRef<RLCrudRef, RLCrudProps>(
318326
{bottomContainerSlot}
319327
</div>
320328
<RLPaginator
321-
page={currentPage}
322-
onPageChange={setCurrentPage}
329+
page={currentPage + 1}
330+
onPageChange={(page) => setCurrentPage(page - 1)}
323331
rowsPerPage={rowsPerPage}
324332
onRowsPerPageChange={setRowsPerPage}
325333
totalRows={totalRows}

src/components/RLCrudFilters/RLCrudFilters.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ export const RLCrudFilters = forwardRef<RLCrudFiltersRef, RLCrudFiltersProps>(
4646

4747
// Initialize on mount
4848
useEffect(() => {
49-
const initialFilters = resetFields()
50-
onFiltersApplied?.(initialFilters)
49+
resetFields()
5150
}, []) // eslint-disable-line react-hooks/exhaustive-deps
5251

5352
const handleApply = useCallback(() => {

src/components/RLDialog/RLDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const RLDialog = forwardRef<RLDialogRef, RLDialogProps>(
8484
<SlDialog
8585
ref={dialogRef}
8686
className={className ?? 'dialog'}
87-
label={label}
87+
label={label ?? ''}
8888
open={open}
8989
noHeader={noHeader}
9090
onSlShow={handleShow}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import type { ComponentType } from 'react'
2+
import { RLCrud } from '../RLCrud'
3+
import type { RLCrudProps } from '../RLCrud/types'
4+
import { addIcon } from '../../icons'
5+
import { ActiveCell } from './cells/ActiveCell'
6+
import { DateCell } from './cells/DateCell'
7+
import { DeleteDialog } from './dialogs/DeleteDialog'
8+
import { usersStore } from './stores/usersStore'
9+
10+
import ghost from '@mdi/svg/svg/ghost.svg'
11+
import deleteIcon from '@mdi/svg/svg/delete.svg'
12+
13+
addIcon('ghost', ghost)
14+
addIcon('delete', deleteIcon)
15+
16+
const users_crud: Omit<RLCrudProps, 'getItems'> = {
17+
id: 'users',
18+
singular_label: 'user',
19+
primary_key: 'id',
20+
filters_title: 'filters',
21+
headers: [
22+
{
23+
i18n_key: 'Username',
24+
sortable: false,
25+
value: 'username',
26+
columnProps: {
27+
className: 'w-1/4'
28+
}
29+
},
30+
{
31+
i18n_key: 'First name',
32+
sortable: false,
33+
value: 'firstName'
34+
},
35+
{
36+
i18n_key: 'Last name',
37+
sortable: false,
38+
value: 'lastName'
39+
},
40+
{
41+
i18n_key: 'Role',
42+
sortable: false,
43+
value: 'role'
44+
},
45+
{
46+
i18n_key: 'Age',
47+
sortable: false,
48+
value: 'age'
49+
},
50+
{
51+
i18n_key: 'Active',
52+
sortable: false,
53+
value: 'active',
54+
type: 'boolean',
55+
componentProps: {
56+
trueColor: 'text-success-500'
57+
}
58+
},
59+
{
60+
i18n_key: 'Activation date',
61+
value: 'activation_date',
62+
sortable: false,
63+
type: 'date'
64+
},
65+
{
66+
i18n_key: 'Expiration date',
67+
value: 'expiration_date',
68+
sortable: false,
69+
type: 'date'
70+
},
71+
{
72+
i18n_key: 'Description',
73+
value: 'description'
74+
}
75+
],
76+
filters: [
77+
{
78+
i18n_key: 'Username',
79+
value: 'username',
80+
input_type: 'text'
81+
},
82+
{
83+
i18n_key: 'First name',
84+
value: 'firstName',
85+
input_type: 'text'
86+
},
87+
{
88+
i18n_key: 'Last name',
89+
value: 'lastName',
90+
input_type: 'text'
91+
},
92+
{
93+
i18n_key: 'Role',
94+
value: 'role',
95+
input_type: 'select',
96+
options: [
97+
{ value: '', text: '' },
98+
{ value: 'admin', text: 'admin' },
99+
{ value: 'user', text: 'user' },
100+
{ value: 'guest', text: 'guest', icon: 'ghost' }
101+
],
102+
default_value: ''
103+
},
104+
{
105+
i18n_key: 'Activation date',
106+
value: 'activation_date',
107+
input_type: 'date'
108+
}
109+
],
110+
form_fields: [
111+
{
112+
i18n_key: 'Username',
113+
value: 'username',
114+
placeholder: 'Enter username',
115+
required: true,
116+
rules: [
117+
{ validateFn: (v: unknown) => !!(v as string), message: 'Username is required' },
118+
{
119+
validateFn: (v: unknown) => (v as string).length > 3,
120+
message: 'Username must be at least 4 characters long'
121+
}
122+
],
123+
side_effect: (model, fields) => {
124+
const { username } = model as { username: string }
125+
if (username === 'admin') {
126+
fields.role.options = [{ value: 'admin', text: 'admin' }]
127+
;(model as { role: string }).role = 'admin'
128+
} else {
129+
fields.role.options = [
130+
{ value: '', text: '' },
131+
{ value: 'admin', text: 'admin' },
132+
{ value: 'user', text: 'user' },
133+
{ value: 'guest', text: 'guest' }
134+
]
135+
}
136+
},
137+
input_type: 'text'
138+
},
139+
{
140+
i18n_key: 'First name',
141+
value: 'firstName',
142+
input_type: 'text'
143+
},
144+
{
145+
i18n_key: 'Last name',
146+
value: 'lastName',
147+
input_type: 'text'
148+
},
149+
{
150+
i18n_key: 'Active',
151+
value: 'active',
152+
input_type: 'checkbox',
153+
default_value: true
154+
},
155+
{
156+
i18n_key: 'Role',
157+
value: 'role',
158+
input_type: 'select',
159+
options: [
160+
{ value: '', text: '' },
161+
{ value: 'admin', text: 'admin' },
162+
{ value: 'user', text: 'user' },
163+
{ value: 'guest', text: 'guest' }
164+
]
165+
},
166+
{
167+
i18n_key: 'Age',
168+
value: 'age',
169+
input_type: 'number'
170+
},
171+
{
172+
i18n_key: 'Activation date',
173+
value: 'activation_date',
174+
input_type: 'date'
175+
},
176+
{
177+
i18n_key: 'Expiration date',
178+
value: 'expiration_date',
179+
input_type: 'date'
180+
},
181+
{
182+
i18n_key: 'Description',
183+
value: 'description',
184+
input_type: 'textarea'
185+
}
186+
],
187+
actions: [
188+
{
189+
name: 'Delete',
190+
i18n_key: 'Delete',
191+
icon_name: 'delete',
192+
onClick: (data: unknown) => {
193+
console.log('Delete side effect', { ...data as object })
194+
},
195+
component: DeleteDialog,
196+
dialogProperties: {
197+
noCloseOnOutsideClick: false
198+
}
199+
}
200+
]
201+
}
202+
203+
export const UsersCrudExample = () => {
204+
return (
205+
<RLCrud
206+
{...users_crud}
207+
getItems={usersStore.getUsers}
208+
addItem={usersStore.createUser}
209+
editItem={usersStore.updateUser}
210+
components={{
211+
boolean: ActiveCell as ComponentType<unknown>,
212+
date: DateCell as ComponentType<unknown>
213+
}}
214+
actionHeaderI18nKey="Actions"
215+
addI18nKey="Add"
216+
applyI18nKey="Apply"
217+
resetI18nKey="Reset"
218+
cancelI18nKey="Cancel"
219+
addButtonI18nKey="Add user"
220+
addTitleI18nKey="Add user"
221+
editTitleI18nKey="Edit user"
222+
editTooltipI18nKey="Edit"
223+
/>
224+
)
225+
}
226+
227+
UsersCrudExample.displayName = 'UsersCrudExample'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { RLIcon } from '../../RLIcon'
2+
import { addIcon } from '../../../icons'
3+
4+
import deleteCircle from '@mdi/svg/svg/delete-circle.svg'
5+
6+
addIcon('deleteCircle', deleteCircle)
7+
8+
interface ActionDeleteProps {
9+
data: unknown
10+
}
11+
12+
export const ActionDelete = (_props: ActionDeleteProps) => {
13+
return (
14+
<RLIcon
15+
className="text-3xl text-red-500 cursor-pointer hover:opacity-40"
16+
name="deleteCircle"
17+
/>
18+
)
19+
}
20+
21+
ActionDelete.displayName = 'ActionDelete'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { RLIcon } from '../../RLIcon'
2+
import { addIcon } from '../../../icons'
3+
4+
import pencilCircle from '@mdi/svg/svg/pencil-circle.svg'
5+
6+
addIcon('pencilCircle', pencilCircle)
7+
8+
interface ActionEditProps {
9+
data: unknown
10+
}
11+
12+
export const ActionEdit = (_props: ActionEditProps) => {
13+
return (
14+
<RLIcon
15+
className="text-3xl text-red-500 cursor-pointer hover:opacity-40"
16+
name="pencilCircle"
17+
/>
18+
)
19+
}
20+
21+
ActionEdit.displayName = 'ActionEdit'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { RLIcon } from '../../RLIcon'
2+
import { addIcon } from '../../../icons'
3+
4+
import checkCircle from '@mdi/svg/svg/check-circle.svg'
5+
import closeCircle from '@mdi/svg/svg/close-circle.svg'
6+
7+
addIcon('checkCircle', checkCircle)
8+
addIcon('closeCircle', closeCircle)
9+
10+
interface ActiveCellProps {
11+
data?: unknown
12+
trueColor?: string
13+
}
14+
15+
export const ActiveCell = ({ data, trueColor = 'text-green-500' }: ActiveCellProps) => {
16+
const typedData = data as { active?: boolean } | undefined
17+
const isActive = typedData?.active
18+
19+
return (
20+
<RLIcon
21+
className={`text-2xl ${isActive ? trueColor : 'text-red-500'}`}
22+
name={isActive ? 'checkCircle' : 'closeCircle'}
23+
/>
24+
)
25+
}
26+
27+
ActiveCell.displayName = 'ActiveCell'
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
interface DateCellProps {
2+
data?: unknown
3+
field?: string
4+
}
5+
6+
export const DateCell = ({ data, field }: DateCellProps) => {
7+
if (!data || !field) {
8+
return <div />
9+
}
10+
11+
const typedData = data as { [key: string]: unknown }
12+
const dateValue = typedData[field] as Date | undefined
13+
14+
if (!dateValue || !(dateValue instanceof Date)) {
15+
return <div />
16+
}
17+
18+
const formattedDate = `${dateValue.getFullYear()}-${('0' + (dateValue.getMonth() + 1)).slice(-2)}-${('0' + dateValue.getDate()).slice(-2)}`
19+
20+
return <div>{formattedDate}</div>
21+
}
22+
23+
DateCell.displayName = 'DateCell'

0 commit comments

Comments
 (0)